@agentforge-ai/cli 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +184 -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/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 +383 -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 +184 -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/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 +383 -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,137 +1,104 @@
|
|
|
1
|
-
import { createFileRoute } from '@tanstack/react-router';
|
|
1
|
+
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
2
2
|
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { Bot, Activity, MessageSquare, FileText, Plus, Heart, Zap, ArrowRight, AlertTriangle, CheckCircle } from 'lucide-react';
|
|
3
|
+
import { useQuery } from 'convex/react';
|
|
4
|
+
import { api } from '@convex/_generated/api';
|
|
5
|
+
import { Bot, MessageSquare, FileText, Activity, Zap, Plus, CheckCircle, AlertTriangle } from 'lucide-react';
|
|
7
6
|
|
|
8
7
|
export const Route = createFileRoute('/')({ component: OverviewPage });
|
|
9
8
|
|
|
10
|
-
function StatCard({ icon: Icon, title, value,
|
|
9
|
+
function StatCard({ icon: Icon, title, value, subtitle }: { icon: any; title: string; value: string | number; subtitle?: string }) {
|
|
11
10
|
return (
|
|
12
|
-
<div className="bg-card
|
|
13
|
-
<div>
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<p className={`text-xs ${change.startsWith('+') ? 'text-green-500' : 'text-red-500'}`}>{change}</p>
|
|
11
|
+
<div className="bg-card border border-border rounded-lg p-6 shadow-sm">
|
|
12
|
+
<div className="flex items-center justify-between mb-2">
|
|
13
|
+
<span className="text-sm font-medium text-muted-foreground">{title}</span>
|
|
14
|
+
<Icon className="w-5 h-5 text-primary" />
|
|
17
15
|
</div>
|
|
18
|
-
<
|
|
16
|
+
<p className="text-3xl font-bold text-foreground">{value}</p>
|
|
17
|
+
{subtitle && <p className="text-xs text-muted-foreground mt-1">{subtitle}</p>}
|
|
19
18
|
</div>
|
|
20
19
|
);
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
function QuickActionButton({ icon: Icon, label, to }) {
|
|
24
|
-
return (
|
|
25
|
-
<a href={to} className="bg-card hover:bg-primary/10 border border-border p-4 rounded-lg flex flex-col items-center justify-center text-center transition-colors">
|
|
26
|
-
<Icon className="w-8 h-8 text-primary mb-2" />
|
|
27
|
-
<span className="text-sm font-medium text-foreground">{label}</span>
|
|
28
|
-
</a>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ActivityItem({ icon: Icon, text, time }) {
|
|
22
|
+
function QuickActionButton({ icon: Icon, label, to }: { icon: any; label: string; to: string }) {
|
|
33
23
|
return (
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<div className="flex-grow">
|
|
39
|
-
<p className="text-sm text-foreground">{text}</p>
|
|
40
|
-
</div>
|
|
41
|
-
<p className="text-xs text-muted-foreground whitespace-nowrap">{time}</p>
|
|
42
|
-
<ArrowRight className="w-4 h-4 text-muted-foreground" />
|
|
43
|
-
</li>
|
|
24
|
+
<Link to={to} className="flex flex-col items-center justify-center p-4 rounded-lg border border-border hover:bg-muted transition-colors gap-2">
|
|
25
|
+
<Icon className="w-6 h-6 text-primary" />
|
|
26
|
+
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
|
27
|
+
</Link>
|
|
44
28
|
);
|
|
45
29
|
}
|
|
46
30
|
|
|
47
|
-
function SystemHealthIndicator({ isHealthy }) {
|
|
48
|
-
const Icon = isHealthy ? CheckCircle : AlertTriangle;
|
|
49
|
-
const text = isHealthy ? 'All systems operational' : 'Service degradation';
|
|
50
|
-
const color = isHealthy ? 'text-green-500' : 'text-yellow-500';
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div className="flex items-center space-x-2">
|
|
54
|
-
<Icon className={`w-5 h-5 ${color}`} />
|
|
55
|
-
<span className={`text-sm font-medium ${color}`}>{text}</span>
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
31
|
function OverviewPage() {
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
totalFiles: 256,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const [activity, setActivity] = useState([
|
|
70
|
-
{ id: 1, icon: Bot, text: 'New agent "SupportBot" created.', time: '5m ago' },
|
|
71
|
-
{ id: 2, icon: MessageSquare, text: 'Session #1245 ended with 52 messages.', time: '1h ago' },
|
|
72
|
-
{ id: 3, icon: FileText, text: 'Uploaded "project_spec.pdf".', time: '3h ago' },
|
|
73
|
-
{ id: 4, icon: Zap, text: 'Agent "DataAnalyzer" completed a task.', time: '5h ago' },
|
|
74
|
-
{ id: 5, icon: Bot, text: 'Agent "CodeGenerator" was updated.', time: '1d ago' },
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
const [systemHealth, setSystemHealth] = useState({ isHealthy: true });
|
|
78
|
-
|
|
79
|
-
// Commented-out Convex hooks for future integration
|
|
80
|
-
/*
|
|
81
|
-
const overviewStats = useQuery(api.overview.getStats);
|
|
82
|
-
const recentActivity = useQuery(api.overview.getRecentActivity, { count: 5 });
|
|
83
|
-
const systemStatus = useQuery(api.heartbeat.getStatus);
|
|
32
|
+
const agents = useQuery(api.agents.list, {});
|
|
33
|
+
const sessions = useQuery(api.sessions.list, {});
|
|
34
|
+
const files = useQuery(api.files.list, {});
|
|
35
|
+
const usage = useQuery(api.usage.list, {});
|
|
36
|
+
const logs = useQuery(api.logs.list, { limit: 5 });
|
|
84
37
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
38
|
+
const totalAgents = agents?.length ?? 0;
|
|
39
|
+
const activeSessions = sessions?.filter((s: any) => s.status === 'active').length ?? 0;
|
|
40
|
+
const totalFiles = files?.length ?? 0;
|
|
41
|
+
const totalTokens = usage?.reduce((sum: number, u: any) => sum + (u.totalTokens || 0), 0) ?? 0;
|
|
42
|
+
const isLoading = agents === undefined;
|
|
90
43
|
|
|
91
44
|
return (
|
|
92
45
|
<DashboardLayout>
|
|
93
|
-
<div className="
|
|
46
|
+
<div className="space-y-8">
|
|
94
47
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
48
|
+
<div>
|
|
49
|
+
<h1 className="text-3xl font-bold">Overview</h1>
|
|
50
|
+
<p className="text-muted-foreground">Welcome back. Here is a snapshot of your workspace.</p>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex items-center space-x-2">
|
|
53
|
+
{isLoading ? (
|
|
54
|
+
<span className="text-sm text-muted-foreground animate-pulse">Connecting to Convex...</span>
|
|
55
|
+
) : (
|
|
56
|
+
<><CheckCircle className="w-5 h-5 text-green-500" /><span className="text-sm font-medium text-green-500">All systems operational</span></>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
100
59
|
</div>
|
|
101
60
|
|
|
102
|
-
{/* Stat Cards */}
|
|
103
61
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
104
|
-
<StatCard icon={Bot} title="Total Agents" value={
|
|
105
|
-
<StatCard icon={Activity} title="Active Sessions" value={
|
|
106
|
-
<StatCard icon={MessageSquare} title="
|
|
107
|
-
<StatCard icon={FileText} title="Total Files" value={
|
|
62
|
+
<StatCard icon={Bot} title="Total Agents" value={totalAgents} subtitle={`${agents?.filter((a: any) => a.isActive).length ?? 0} active`} />
|
|
63
|
+
<StatCard icon={Activity} title="Active Sessions" value={activeSessions} />
|
|
64
|
+
<StatCard icon={MessageSquare} title="Total Tokens Used" value={totalTokens.toLocaleString()} />
|
|
65
|
+
<StatCard icon={FileText} title="Total Files" value={totalFiles} />
|
|
108
66
|
</div>
|
|
109
67
|
|
|
110
68
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{activity.length > 0 ? (
|
|
69
|
+
<div className="lg:col-span-2 bg-card p-6 rounded-lg shadow-sm border border-border">
|
|
70
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center"><Activity className="w-6 h-6 mr-2 text-primary" />Recent Activity</h2>
|
|
71
|
+
{logs && logs.length > 0 ? (
|
|
115
72
|
<ul>
|
|
116
|
-
{
|
|
117
|
-
<
|
|
73
|
+
{logs.map((log: any) => (
|
|
74
|
+
<li key={log._id} className="flex items-center space-x-4 py-3 border-b border-border last:border-b-0">
|
|
75
|
+
<div className="bg-primary/10 p-2 rounded-full">
|
|
76
|
+
{log.level === 'error' ? <AlertTriangle className="w-5 h-5 text-destructive" /> : <Activity className="w-5 h-5 text-primary" />}
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex-grow min-w-0">
|
|
79
|
+
<p className="text-sm text-foreground truncate">{log.message}</p>
|
|
80
|
+
<p className="text-xs text-muted-foreground">{log.source}</p>
|
|
81
|
+
</div>
|
|
82
|
+
<p className="text-xs text-muted-foreground whitespace-nowrap">{new Date(log.timestamp).toLocaleString()}</p>
|
|
83
|
+
</li>
|
|
118
84
|
))}
|
|
119
85
|
</ul>
|
|
120
86
|
) : (
|
|
121
87
|
<div className="text-center py-10">
|
|
122
|
-
<
|
|
88
|
+
<Activity className="w-12 h-12 text-muted-foreground/30 mx-auto mb-3" />
|
|
89
|
+
<p className="text-muted-foreground">No recent activity yet.</p>
|
|
90
|
+
<p className="text-sm text-muted-foreground mt-1">Activity will appear here as you use your agents.</p>
|
|
123
91
|
</div>
|
|
124
92
|
)}
|
|
125
93
|
</div>
|
|
126
94
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<h2 className="text-xl font-semibold mb-4 flex items-center"><Zap className="w-6 h-6 mr-2 text-primary"/>Quick Actions</h2>
|
|
95
|
+
<div className="bg-card p-6 rounded-lg shadow-sm border border-border">
|
|
96
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center"><Zap className="w-6 h-6 mr-2 text-primary" />Quick Actions</h2>
|
|
130
97
|
<div className="grid grid-cols-2 gap-4">
|
|
131
|
-
<QuickActionButton icon={Bot} label="Create Agent" to="/agents
|
|
98
|
+
<QuickActionButton icon={Bot} label="Create Agent" to="/agents" />
|
|
132
99
|
<QuickActionButton icon={MessageSquare} label="Start Chat" to="/chat" />
|
|
133
100
|
<QuickActionButton icon={FileText} label="Upload File" to="/files" />
|
|
134
|
-
<QuickActionButton icon={Plus} label="New Project" to="/projects
|
|
101
|
+
<QuickActionButton icon={Plus} label="New Project" to="/projects" />
|
|
135
102
|
</div>
|
|
136
103
|
</div>
|
|
137
104
|
</div>
|
|
@@ -1,254 +1,135 @@
|
|
|
1
|
-
import { createFileRoute
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
2
|
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { FolderKanban, Plus,
|
|
7
|
-
import * as Dialog from '@radix-ui/react-dialog';
|
|
8
|
-
import * as Tabs from '@radix-ui/react-tabs';
|
|
9
|
-
|
|
10
|
-
// --- MOCK DATA & TYPES ---
|
|
11
|
-
type Project = {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
description: string;
|
|
15
|
-
agentCount: number;
|
|
16
|
-
fileCount: number;
|
|
17
|
-
createdAt: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const initialProjects: Project[] = [
|
|
21
|
-
{ id: 'proj_1', name: 'AgentForge UI Revamp', description: 'Complete overhaul of the dashboard UI using TanStack and Tailwind.', agentCount: 5, fileCount: 42, createdAt: '2024-07-21' },
|
|
22
|
-
{ id: 'proj_2', name: 'Customer Support Bot', description: 'An AI-powered chatbot to handle customer queries in real-time.', agentCount: 2, fileCount: 15, createdAt: '2024-07-20' },
|
|
23
|
-
{ id: 'proj_3', name: 'Data Analysis Pipeline', description: 'Automated workflow for processing and analyzing sales data.', agentCount: 8, fileCount: 128, createdAt: '2024-06-15' },
|
|
24
|
-
{ id: 'proj_4', name: 'Internal Documentation Hub', description: 'A centralized place for all internal project documentation.', agentCount: 1, fileCount: 256, createdAt: '2024-05-01' },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
// --- HELPER COMPONENTS ---
|
|
28
|
-
|
|
29
|
-
const ProjectCard = ({ project, onEdit, onDelete, onSelect }: { project: Project, onEdit: (project: Project) => void, onDelete: (id: string) => void, onSelect: (project: Project) => void }) => (
|
|
30
|
-
<div className="bg-card border border-border rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex flex-col">
|
|
31
|
-
<div className="p-4 flex-grow cursor-pointer" onClick={() => onSelect(project)}>
|
|
32
|
-
<div className="flex items-start justify-between">
|
|
33
|
-
<h3 className="font-bold text-lg text-foreground mb-2">{project.name}</h3>
|
|
34
|
-
<FolderKanban className="w-6 h-6 text-muted-foreground" />
|
|
35
|
-
</div>
|
|
36
|
-
<p className="text-sm text-muted-foreground mb-4 h-10 overflow-hidden">{project.description}</p>
|
|
37
|
-
</div>
|
|
38
|
-
<div className="border-t border-border p-4 flex justify-between items-center text-xs text-muted-foreground">
|
|
39
|
-
<div className="flex items-center gap-4">
|
|
40
|
-
<span className="flex items-center gap-1"><Users className="w-4 h-4" /> {project.agentCount}</span>
|
|
41
|
-
<span className="flex items-center gap-1"><FileText className="w-4 h-4" /> {project.fileCount}</span>
|
|
42
|
-
</div>
|
|
43
|
-
<div className="flex items-center gap-2">
|
|
44
|
-
<button onClick={() => onEdit(project)} className="hover:text-foreground"><Edit className="w-4 h-4" /></button>
|
|
45
|
-
<button onClick={() => onDelete(project.id)} className="hover:text-destructive"><Trash2 className="w-4 h-4" /></button>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const ProjectForm = ({ project, onSave, onCancel }: { project?: Project | null, onSave: (p: Omit<Project, 'id' | 'agentCount' | 'fileCount' | 'createdAt'>) => void, onCancel: () => void }) => {
|
|
52
|
-
const [name, setName] = useState(project?.name || '');
|
|
53
|
-
const [description, setDescription] = useState(project?.description || '');
|
|
54
|
-
|
|
55
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
56
|
-
e.preventDefault();
|
|
57
|
-
if (!name) return;
|
|
58
|
-
onSave({ name, description });
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
63
|
-
<div>
|
|
64
|
-
<label htmlFor="name" className="block text-sm font-medium text-muted-foreground mb-1">Project Name</label>
|
|
65
|
-
<input id="name" value={name} onChange={e => setName(e.target.value)} className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:ring-2 focus:ring-primary" required />
|
|
66
|
-
</div>
|
|
67
|
-
<div>
|
|
68
|
-
<label htmlFor="description" className="block text-sm font-medium text-muted-foreground mb-1">Description</label>
|
|
69
|
-
<textarea id="description" value={description} onChange={e => setDescription(e.target.value)} rows={4} className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:ring-2 focus:ring-primary" />
|
|
70
|
-
</div>
|
|
71
|
-
<div className="flex justify-end gap-2 mt-4">
|
|
72
|
-
<button type="button" onClick={onCancel} className="px-4 py-2 rounded-md border border-border text-foreground hover:bg-card">Cancel</button>
|
|
73
|
-
<button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90">Save Project</button>
|
|
74
|
-
</div>
|
|
75
|
-
</form>
|
|
76
|
-
);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const ProjectDetailView = ({ project, onBack }: { project: Project, onBack: () => void }) => (
|
|
80
|
-
<div className="bg-background p-6 rounded-lg">
|
|
81
|
-
<div className="flex justify-between items-start mb-6">
|
|
82
|
-
<div>
|
|
83
|
-
<button onClick={onBack} className="text-sm text-primary hover:underline mb-2">← Back to Projects</button>
|
|
84
|
-
<h2 className="text-2xl font-bold text-foreground">{project.name}</h2>
|
|
85
|
-
<p className="text-muted-foreground">{project.description}</p>
|
|
86
|
-
</div>
|
|
87
|
-
<div className="text-sm text-muted-foreground">Created: {new Date(project.createdAt).toLocaleDateString()}</div>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
<Tabs.Root defaultValue="overview" className="w-full">
|
|
91
|
-
<Tabs.List className="flex border-b border-border mb-4">
|
|
92
|
-
<Tabs.Trigger value="overview" className="px-4 py-2 text-sm font-medium text-muted-foreground data-[state=active]:text-foreground data-[state=active]:border-b-2 data-[state=active]:border-primary">Overview</Tabs.Trigger>
|
|
93
|
-
<Tabs.Trigger value="agents" className="px-4 py-2 text-sm font-medium text-muted-foreground data-[state=active]:text-foreground data-[state=active]:border-b-2 data-[state=active]:border-primary">Agents</Tabs.Trigger>
|
|
94
|
-
<Tabs.Trigger value="files" className="px-4 py-2 text-sm font-medium text-muted-foreground data-[state=active]:text-foreground data-[state=active]:border-b-2 data-[state=active]:border-primary">Files</Tabs.Trigger>
|
|
95
|
-
<Tabs.Trigger value="settings" className="px-4 py-2 text-sm font-medium text-muted-foreground data-[state=active]:text-foreground data-[state=active]:border-b-2 data-[state=active]:border-primary">Settings</Tabs.Trigger>
|
|
96
|
-
</Tabs.List>
|
|
97
|
-
<Tabs.Content value="overview" className="p-4 bg-card rounded-b-lg border border-t-0 border-border">
|
|
98
|
-
<h4 className="font-bold text-lg mb-2">Project Overview</h4>
|
|
99
|
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
|
100
|
-
<div className="bg-background p-4 rounded-lg border border-border"><p className="text-2xl font-bold">{project.agentCount}</p><p className="text-sm text-muted-foreground">Agents</p></div>
|
|
101
|
-
<div className="bg-background p-4 rounded-lg border border-border"><p className="text-2xl font-bold">{project.fileCount}</p><p className="text-sm text-muted-foreground">Files</p></div>
|
|
102
|
-
</div>
|
|
103
|
-
</Tabs.Content>
|
|
104
|
-
<Tabs.Content value="agents" className="p-4 bg-card rounded-b-lg border border-t-0 border-border"><p className="text-muted-foreground">Agent management UI will be here.</p></Tabs.Content>
|
|
105
|
-
<Tabs.Content value="files" className="p-4 bg-card rounded-b-lg border border-t-0 border-border"><p className="text-muted-foreground">File management UI will be here.</p></Tabs.Content>
|
|
106
|
-
<Tabs.Content value="settings" className="p-4 bg-card rounded-b-lg border border-t-0 border-border"><p className="text-muted-foreground">Project settings UI will be here.</p></Tabs.Content>
|
|
107
|
-
</Tabs.Root>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
// --- MAIN PAGE COMPONENT ---
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import { useQuery, useMutation } from 'convex/react';
|
|
5
|
+
import { api } from '@convex/_generated/api';
|
|
6
|
+
import { FolderKanban, Plus, Trash2, Edit, Search, X } from 'lucide-react';
|
|
112
7
|
|
|
113
8
|
export const Route = createFileRoute('/projects')({ component: ProjectsPage });
|
|
114
9
|
|
|
115
10
|
function ProjectsPage() {
|
|
116
|
-
const
|
|
117
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
118
|
-
const [error, setError] = useState<string | null>(null);
|
|
119
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
120
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
121
|
-
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
122
|
-
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
|
123
|
-
|
|
124
|
-
// --- Commented-out Convex Hooks ---
|
|
125
|
-
/*
|
|
126
|
-
const projectsData = useQuery(api.projects.list, { search: searchQuery });
|
|
11
|
+
const projects = useQuery(api.projects.list, {}) ?? [];
|
|
127
12
|
const createProject = useMutation(api.projects.create);
|
|
128
13
|
const updateProject = useMutation(api.projects.update);
|
|
129
|
-
const
|
|
14
|
+
const removeProject = useMutation(api.projects.remove);
|
|
130
15
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
16
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
17
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
18
|
+
const [editingProject, setEditingProject] = useState<any>(null);
|
|
19
|
+
|
|
20
|
+
const filtered = useMemo(() => {
|
|
21
|
+
if (!searchQuery) return projects;
|
|
22
|
+
const q = searchQuery.toLowerCase();
|
|
23
|
+
return projects.filter((p: any) => p.name.toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q));
|
|
24
|
+
}, [projects, searchQuery]);
|
|
25
|
+
|
|
26
|
+
const handleSave = async (data: { name: string; description: string }) => {
|
|
27
|
+
if (editingProject) {
|
|
28
|
+
await updateProject({ id: editingProject._id, ...data });
|
|
29
|
+
} else {
|
|
30
|
+
await createProject(data);
|
|
134
31
|
}
|
|
135
|
-
|
|
136
|
-
*/
|
|
137
|
-
|
|
138
|
-
const handleCreate = () => {
|
|
32
|
+
setIsModalOpen(false);
|
|
139
33
|
setEditingProject(null);
|
|
140
|
-
setIsModalOpen(true);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const handleEdit = (project: Project) => {
|
|
144
|
-
setEditingProject(project);
|
|
145
|
-
setIsModalOpen(true);
|
|
146
34
|
};
|
|
147
35
|
|
|
148
|
-
const handleDelete = (id:
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
setProjects(projects.filter(p => p.id !== id));
|
|
36
|
+
const handleDelete = async (id: any) => {
|
|
37
|
+
if (confirm('Delete this project?')) {
|
|
38
|
+
await removeProject({ id });
|
|
152
39
|
}
|
|
153
40
|
};
|
|
154
41
|
|
|
155
|
-
const handleSave = (data: Omit<Project, 'id' | 'agentCount' | 'fileCount' | 'createdAt'>) => {
|
|
156
|
-
setIsLoading(true);
|
|
157
|
-
setTimeout(() => { // Simulate async operation
|
|
158
|
-
if (editingProject) {
|
|
159
|
-
// await updateProject({ id: editingProject.id, ...data });
|
|
160
|
-
setProjects(projects.map(p => p.id === editingProject.id ? { ...p, ...data } : p));
|
|
161
|
-
} else {
|
|
162
|
-
const newProject: Project = {
|
|
163
|
-
id: `proj_${Date.now()}`,
|
|
164
|
-
...data,
|
|
165
|
-
agentCount: 0,
|
|
166
|
-
fileCount: 0,
|
|
167
|
-
createdAt: new Date().toISOString().split('T')[0],
|
|
168
|
-
};
|
|
169
|
-
// await createProject(newProject);
|
|
170
|
-
setProjects([newProject, ...projects]);
|
|
171
|
-
}
|
|
172
|
-
setIsLoading(false);
|
|
173
|
-
setIsModalOpen(false);
|
|
174
|
-
setEditingProject(null);
|
|
175
|
-
}, 500);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const filteredProjects = useMemo(() =>
|
|
179
|
-
projects.filter(p =>
|
|
180
|
-
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
181
|
-
p.description.toLowerCase().includes(searchQuery.toLowerCase())
|
|
182
|
-
), [projects, searchQuery]);
|
|
183
|
-
|
|
184
|
-
if (selectedProject) {
|
|
185
|
-
return (
|
|
186
|
-
<DashboardLayout>
|
|
187
|
-
<ProjectDetailView project={selectedProject} onBack={() => setSelectedProject(null)} />
|
|
188
|
-
</DashboardLayout>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
42
|
return (
|
|
193
43
|
<DashboardLayout>
|
|
194
|
-
<div className="
|
|
195
|
-
<div className="flex justify-between items-center
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
<
|
|
199
|
-
|
|
44
|
+
<div className="space-y-6">
|
|
45
|
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
46
|
+
<div>
|
|
47
|
+
<h1 className="text-3xl font-bold">Projects</h1>
|
|
48
|
+
<p className="text-muted-foreground">Organize your agents, files, and threads into workspaces.</p>
|
|
49
|
+
</div>
|
|
50
|
+
<button onClick={() => { setEditingProject(null); setIsModalOpen(true); }} className="bg-primary text-primary-foreground px-4 py-2 rounded-lg hover:bg-primary/90 flex items-center gap-2">
|
|
51
|
+
<Plus className="w-4 h-4" /> New Project
|
|
200
52
|
</button>
|
|
201
53
|
</div>
|
|
202
54
|
|
|
203
|
-
<div className="
|
|
204
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2
|
|
205
|
-
<input
|
|
206
|
-
type="text"
|
|
207
|
-
placeholder="Search projects..."
|
|
208
|
-
value={searchQuery}
|
|
209
|
-
onChange={e => setSearchQuery(e.target.value)}
|
|
210
|
-
className="w-full bg-card border border-border rounded-md pl-10 pr-4 py-2 text-foreground"
|
|
211
|
-
/>
|
|
55
|
+
<div className="relative max-w-sm">
|
|
56
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
57
|
+
<input type="text" placeholder="Search projects..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-9 pr-3 py-2 bg-card border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary w-full" />
|
|
212
58
|
</div>
|
|
213
59
|
|
|
214
|
-
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
<
|
|
222
|
-
))}
|
|
223
|
-
</div>
|
|
224
|
-
) : (
|
|
225
|
-
<div className="text-center py-16 border-2 border-dashed border-border rounded-lg">
|
|
226
|
-
<FolderKanban className="mx-auto w-12 h-12 text-muted-foreground" />
|
|
227
|
-
<h3 className="mt-4 text-lg font-medium text-foreground">No projects found</h3>
|
|
228
|
-
<p className="mt-1 text-sm text-muted-foreground">Get started by creating a new project.</p>
|
|
229
|
-
<button onClick={handleCreate} className="mt-6 flex mx-auto items-center gap-2 bg-primary text-primary-foreground px-4 py-2 rounded-md hover:bg-primary/90">
|
|
230
|
-
<Plus className="w-5 h-5" />
|
|
231
|
-
New Project
|
|
60
|
+
{filtered.length === 0 ? (
|
|
61
|
+
<div className="text-center py-16 bg-card border border-border rounded-lg">
|
|
62
|
+
<FolderKanban className="w-16 h-16 text-muted-foreground/30 mx-auto mb-4" />
|
|
63
|
+
<h3 className="text-lg font-semibold mb-2">{projects.length === 0 ? 'No projects yet' : 'No matching projects'}</h3>
|
|
64
|
+
<p className="text-muted-foreground mb-4">Create a project to organize your work.</p>
|
|
65
|
+
{projects.length === 0 && (
|
|
66
|
+
<button onClick={() => setIsModalOpen(true)} className="bg-primary text-primary-foreground px-4 py-2 rounded-lg hover:bg-primary/90">
|
|
67
|
+
<Plus className="w-4 h-4 inline mr-2" />Create Project
|
|
232
68
|
</button>
|
|
233
|
-
|
|
234
|
-
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
73
|
+
{filtered.map((project: any) => (
|
|
74
|
+
<div key={project._id} className="bg-card border border-border rounded-lg p-5 shadow-sm hover:shadow-md transition-shadow">
|
|
75
|
+
<div className="flex items-start justify-between mb-3">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<FolderKanban className="w-5 h-5 text-primary" />
|
|
78
|
+
<h3 className="font-semibold text-foreground">{project.name}</h3>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">{project.description || 'No description'}</p>
|
|
82
|
+
<div className="text-xs text-muted-foreground mb-4">Created {new Date(project.createdAt).toLocaleDateString()}</div>
|
|
83
|
+
<div className="flex items-center justify-between pt-3 border-t border-border">
|
|
84
|
+
<button onClick={() => { setEditingProject(project); setIsModalOpen(true); }} className="p-1.5 rounded hover:bg-muted flex items-center gap-1 text-xs text-muted-foreground"><Edit className="w-4 h-4" /> Edit</button>
|
|
85
|
+
<button onClick={() => handleDelete(project._id)} className="p-1.5 rounded hover:bg-destructive/10"><Trash2 className="w-4 h-4 text-destructive" /></button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
235
90
|
)}
|
|
236
91
|
|
|
237
|
-
|
|
238
|
-
<
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
</Dialog.Content>
|
|
249
|
-
</Dialog.Portal>
|
|
250
|
-
</Dialog.Root>
|
|
92
|
+
{isModalOpen && (
|
|
93
|
+
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
|
94
|
+
<div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-md">
|
|
95
|
+
<div className="flex justify-between items-center p-4 border-b border-border">
|
|
96
|
+
<h2 className="text-lg font-bold">{editingProject ? 'Edit Project' : 'New Project'}</h2>
|
|
97
|
+
<button onClick={() => { setIsModalOpen(false); setEditingProject(null); }} className="text-muted-foreground hover:text-foreground"><X className="h-5 w-5" /></button>
|
|
98
|
+
</div>
|
|
99
|
+
<ProjectForm initial={editingProject} onSave={handleSave} onClose={() => { setIsModalOpen(false); setEditingProject(null); }} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
251
103
|
</div>
|
|
252
104
|
</DashboardLayout>
|
|
253
105
|
);
|
|
254
106
|
}
|
|
107
|
+
|
|
108
|
+
function ProjectForm({ initial, onSave, onClose }: { initial: any; onSave: (data: any) => void; onClose: () => void }) {
|
|
109
|
+
const [name, setName] = useState(initial?.name || '');
|
|
110
|
+
const [description, setDescription] = useState(initial?.description || '');
|
|
111
|
+
|
|
112
|
+
const handleSubmit = (e: any) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
onSave({ name, description });
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<form onSubmit={handleSubmit}>
|
|
119
|
+
<div className="p-6 space-y-4">
|
|
120
|
+
<div>
|
|
121
|
+
<label className="block text-sm font-medium mb-1">Name</label>
|
|
122
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm" placeholder="Project name" required />
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<label className="block text-sm font-medium mb-1">Description</label>
|
|
126
|
+
<textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={3} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm" placeholder="What is this project about?" />
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="p-4 border-t border-border flex justify-end gap-2">
|
|
130
|
+
<button type="button" onClick={onClose} className="px-4 py-2 rounded-lg bg-muted text-muted-foreground text-sm">Cancel</button>
|
|
131
|
+
<button type="submit" disabled={!name.trim()} className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 disabled:opacity-50">Save</button>
|
|
132
|
+
</div>
|
|
133
|
+
</form>
|
|
134
|
+
);
|
|
135
|
+
}
|