@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,141 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
// import { useQuery } from 'convex/react';
|
|
5
|
+
// import { api } from '~/../convex/_generated/api';
|
|
6
|
+
import { Bot, Activity, MessageSquare, FileText, Plus, Heart, Zap, ArrowRight, AlertTriangle, CheckCircle } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute('/')({ component: OverviewPage });
|
|
9
|
+
|
|
10
|
+
function StatCard({ icon: Icon, title, value, change }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="bg-card p-6 rounded-lg shadow-md flex items-center justify-between">
|
|
13
|
+
<div>
|
|
14
|
+
<p className="text-sm text-muted-foreground">{title}</p>
|
|
15
|
+
<p className="text-3xl font-bold text-foreground">{value}</p>
|
|
16
|
+
<p className={`text-xs ${change.startsWith('+') ? 'text-green-500' : 'text-red-500'}`}>{change}</p>
|
|
17
|
+
</div>
|
|
18
|
+
<Icon className="w-10 h-10 text-primary" />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
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 }) {
|
|
33
|
+
return (
|
|
34
|
+
<li className="flex items-center space-x-4 py-3 border-b border-border last:border-b-0">
|
|
35
|
+
<div className="bg-primary/10 p-2 rounded-full">
|
|
36
|
+
<Icon className="w-5 h-5 text-primary" />
|
|
37
|
+
</div>
|
|
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>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
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
|
+
function OverviewPage() {
|
|
61
|
+
// Local state with realistic initial values, simulating data from Convex
|
|
62
|
+
const [stats, setStats] = useState({
|
|
63
|
+
totalAgents: 12,
|
|
64
|
+
activeSessions: 3,
|
|
65
|
+
messagesToday: 1402,
|
|
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);
|
|
84
|
+
|
|
85
|
+
// In a real implementation, you would use the data from hooks:
|
|
86
|
+
// const stats = overviewStats || { totalAgents: 0, activeSessions: 0, messagesToday: 0, totalFiles: 0 };
|
|
87
|
+
// const activity = recentActivity || [];
|
|
88
|
+
// const systemHealth = { isHealthy: systemStatus === 'operational' };
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<DashboardLayout>
|
|
93
|
+
<div className="p-6 md:p-8 space-y-8 bg-background text-foreground">
|
|
94
|
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
95
|
+
<div>
|
|
96
|
+
<h1 className="text-3xl font-bold">Overview</h1>
|
|
97
|
+
<p className="text-muted-foreground">Welcome back, here's a snapshot of your workspace.</p>
|
|
98
|
+
</div>
|
|
99
|
+
<SystemHealthIndicator isHealthy={systemHealth.isHealthy} />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Stat Cards */}
|
|
103
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
104
|
+
<StatCard icon={Bot} title="Total Agents" value={stats.totalAgents} change="+2 this week" />
|
|
105
|
+
<StatCard icon={Activity} title="Active Sessions" value={stats.activeSessions} change="-1 since yesterday" />
|
|
106
|
+
<StatCard icon={MessageSquare} title="Messages Today" value={stats.messagesToday.toLocaleString()} change="+15%" />
|
|
107
|
+
<StatCard icon={FileText} title="Total Files" value={stats.totalFiles} change="+12 files" />
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
111
|
+
{/* Recent Activity */}
|
|
112
|
+
<div className="lg:col-span-2 bg-card p-6 rounded-lg shadow-md">
|
|
113
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center"><Activity className="w-6 h-6 mr-2 text-primary"/>Recent Activity</h2>
|
|
114
|
+
{activity.length > 0 ? (
|
|
115
|
+
<ul>
|
|
116
|
+
{activity.map(item => (
|
|
117
|
+
<ActivityItem key={item.id} icon={item.icon} text={item.text} time={item.time} />
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="text-center py-10">
|
|
122
|
+
<p className="text-muted-foreground">No recent activity to display.</p>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Quick Actions */}
|
|
128
|
+
<div className="bg-card p-6 rounded-lg shadow-md">
|
|
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>
|
|
130
|
+
<div className="grid grid-cols-2 gap-4">
|
|
131
|
+
<QuickActionButton icon={Bot} label="Create Agent" to="/agents/new" />
|
|
132
|
+
<QuickActionButton icon={MessageSquare} label="Start Chat" to="/chat" />
|
|
133
|
+
<QuickActionButton icon={FileText} label="Upload File" to="/files" />
|
|
134
|
+
<QuickActionButton icon={Plus} label="New Project" to="/projects/new" />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</DashboardLayout>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
2
|
+
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
|
+
import React, { useState, useMemo } from 'react';
|
|
4
|
+
// import { useQuery, useMutation } from 'convex/react';
|
|
5
|
+
// import { api } from '../../convex/_generated/api';
|
|
6
|
+
import { FolderKanban, Plus, Settings, Users, FileText, Trash2, Edit, X, Search, MoreVertical } from 'lucide-react';
|
|
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 ---
|
|
112
|
+
|
|
113
|
+
export const Route = createFileRoute('/projects')({ component: ProjectsPage });
|
|
114
|
+
|
|
115
|
+
function ProjectsPage() {
|
|
116
|
+
const [projects, setProjects] = useState<Project[]>(initialProjects);
|
|
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 });
|
|
127
|
+
const createProject = useMutation(api.projects.create);
|
|
128
|
+
const updateProject = useMutation(api.projects.update);
|
|
129
|
+
const deleteProject = useMutation(api.projects.delete);
|
|
130
|
+
|
|
131
|
+
React.useEffect(() => {
|
|
132
|
+
if (projectsData) {
|
|
133
|
+
setProjects(projectsData);
|
|
134
|
+
}
|
|
135
|
+
}, [projectsData]);
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
const handleCreate = () => {
|
|
139
|
+
setEditingProject(null);
|
|
140
|
+
setIsModalOpen(true);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleEdit = (project: Project) => {
|
|
144
|
+
setEditingProject(project);
|
|
145
|
+
setIsModalOpen(true);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleDelete = (id: string) => {
|
|
149
|
+
if (window.confirm('Are you sure you want to delete this project?')) {
|
|
150
|
+
// await deleteProject({ id });
|
|
151
|
+
setProjects(projects.filter(p => p.id !== id));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
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
|
+
return (
|
|
193
|
+
<DashboardLayout>
|
|
194
|
+
<div className="p-6 bg-background min-h-full">
|
|
195
|
+
<div className="flex justify-between items-center mb-6">
|
|
196
|
+
<h1 className="text-2xl font-bold text-foreground">Projects</h1>
|
|
197
|
+
<button onClick={handleCreate} className="flex items-center gap-2 bg-primary text-primary-foreground px-4 py-2 rounded-md hover:bg-primary/90">
|
|
198
|
+
<Plus className="w-5 h-5" />
|
|
199
|
+
New Project
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="mb-6 relative">
|
|
204
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
|
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
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{isLoading && <p className="text-muted-foreground">Loading projects...</p>}
|
|
215
|
+
{error && <p className="text-destructive">{error}</p>}
|
|
216
|
+
|
|
217
|
+
{!isLoading && !error && (
|
|
218
|
+
filteredProjects.length > 0 ? (
|
|
219
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
220
|
+
{filteredProjects.map(project => (
|
|
221
|
+
<ProjectCard key={project.id} project={project} onEdit={handleEdit} onDelete={handleDelete} onSelect={setSelectedProject} />
|
|
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
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
<Dialog.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
238
|
+
<Dialog.Portal>
|
|
239
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
|
|
240
|
+
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] max-w-md bg-card p-6 rounded-lg shadow-lg border border-border">
|
|
241
|
+
<Dialog.Title className="text-lg font-bold text-foreground mb-4">{editingProject ? 'Edit Project' : 'Create New Project'}</Dialog.Title>
|
|
242
|
+
<ProjectForm project={editingProject} onSave={handleSave} onCancel={() => setIsModalOpen(false)} />
|
|
243
|
+
<Dialog.Close asChild>
|
|
244
|
+
<button className="absolute top-4 right-4 text-muted-foreground hover:text-foreground">
|
|
245
|
+
<X className="w-5 h-5" />
|
|
246
|
+
</button>
|
|
247
|
+
</Dialog.Close>
|
|
248
|
+
</Dialog.Content>
|
|
249
|
+
</Dialog.Portal>
|
|
250
|
+
</Dialog.Root>
|
|
251
|
+
</div>
|
|
252
|
+
</DashboardLayout>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
// import { useQuery, useMutation } from 'convex/react';
|
|
5
|
+
// import { api } from '../../convex/_generated/api';
|
|
6
|
+
import { Button } from '../components/ui/button';
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../components/ui/table';
|
|
9
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
|
10
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose } from '../components/ui/sheet';
|
|
11
|
+
import { Badge } from '../components/ui/badge';
|
|
12
|
+
import { Activity, Clock, MessageSquare, XCircle, Loader2 } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
// Mock data structure, aligned with the requirements
|
|
15
|
+
const mockSessions = [
|
|
16
|
+
{
|
|
17
|
+
_id: 's1',
|
|
18
|
+
agentId: 'agent-001',
|
|
19
|
+
status: 'active',
|
|
20
|
+
_creationTime: new Date().getTime() - 1000 * 60 * 5,
|
|
21
|
+
messageCount: 42,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
_id: 's2',
|
|
25
|
+
agentId: 'agent-007',
|
|
26
|
+
status: 'idle',
|
|
27
|
+
_creationTime: new Date().getTime() - 1000 * 60 * 30,
|
|
28
|
+
messageCount: 12,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
_id: 's3',
|
|
32
|
+
agentId: 'agent-002',
|
|
33
|
+
status: 'ended',
|
|
34
|
+
_creationTime: new Date().getTime() - 1000 * 60 * 120,
|
|
35
|
+
endTime: new Date().getTime() - 1000 * 60 * 60,
|
|
36
|
+
messageCount: 150,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
_id: 's4',
|
|
40
|
+
agentId: 'agent-001',
|
|
41
|
+
status: 'active',
|
|
42
|
+
_creationTime: new Date().getTime() - 1000 * 60 * 2,
|
|
43
|
+
messageCount: 5,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
type Session = typeof mockSessions[0] & { endTime?: number };
|
|
48
|
+
|
|
49
|
+
export const Route = createFileRoute('/sessions')({
|
|
50
|
+
component: SessionsPage,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function formatDuration(startTime: number, endTime?: number) {
|
|
54
|
+
const end = endTime ? endTime : new Date().getTime();
|
|
55
|
+
const seconds = Math.floor((end - startTime) / 1000);
|
|
56
|
+
if (seconds < 60) return `${seconds}s`;
|
|
57
|
+
const minutes = Math.floor(seconds / 60);
|
|
58
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
59
|
+
const hours = Math.floor(minutes / 60);
|
|
60
|
+
return `${hours}h ${minutes % 60}m`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function SessionsPage() {
|
|
64
|
+
const [sessions, setSessions] = useState<Session[]>([]);
|
|
65
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
68
|
+
const [selectedSession, setSelectedSession] = useState<Session | null>(null);
|
|
69
|
+
|
|
70
|
+
// --- Commented-out Convex Hooks ---
|
|
71
|
+
/*
|
|
72
|
+
const sessionsData = useQuery(
|
|
73
|
+
api.sessions.listByStatus,
|
|
74
|
+
statusFilter === 'all' ? {} : { status: statusFilter }
|
|
75
|
+
);
|
|
76
|
+
const endSessionMutation = useMutation(api.sessions.endSession);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (sessionsData === undefined) {
|
|
80
|
+
setIsLoading(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
} else if (sessionsData) {
|
|
83
|
+
setSessions(sessionsData);
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
} else {
|
|
86
|
+
// Handle potential query error from Convex
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
setError("Failed to fetch sessions.");
|
|
89
|
+
}
|
|
90
|
+
}, [sessionsData]);
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
// Local state simulation of fetching data
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setIsLoading(true);
|
|
96
|
+
setError(null);
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
try {
|
|
99
|
+
// Simulate API call
|
|
100
|
+
setSessions(mockSessions.map(s => ({...s, endTime: s.status === 'ended' ? (s as any).endTime : undefined })));
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
setError("Failed to load sessions.");
|
|
104
|
+
setIsLoading(false);
|
|
105
|
+
}
|
|
106
|
+
}, 1000);
|
|
107
|
+
return () => clearTimeout(timer);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const handleEndSession = async (sessionId: string) => {
|
|
111
|
+
// Optimistic update for local state
|
|
112
|
+
setSessions(prev =>
|
|
113
|
+
prev.map(s =>
|
|
114
|
+
s._id === sessionId ? { ...s, status: 'ended', endTime: new Date().getTime() } : s
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// --- Commented-out Convex Mutation Call ---
|
|
119
|
+
/*
|
|
120
|
+
try {
|
|
121
|
+
await endSessionMutation({ sessionId });
|
|
122
|
+
// Optionally, you can show a toast notification for success
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error("Failed to end session:", err);
|
|
125
|
+
// Revert optimistic update on failure
|
|
126
|
+
setSessions(sessionsData || []); // Revert to original data from query
|
|
127
|
+
// Optionally, show an error toast
|
|
128
|
+
}
|
|
129
|
+
*/
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const filteredSessions = useMemo(() => {
|
|
133
|
+
if (statusFilter === 'all') {
|
|
134
|
+
return sessions;
|
|
135
|
+
}
|
|
136
|
+
return sessions.filter(s => s.status === statusFilter);
|
|
137
|
+
}, [sessions, statusFilter]);
|
|
138
|
+
|
|
139
|
+
const statusBadge = (status: string) => {
|
|
140
|
+
switch (status) {
|
|
141
|
+
case 'active':
|
|
142
|
+
return <Badge variant="default" className="bg-green-500 text-white">Active</Badge>;
|
|
143
|
+
case 'idle':
|
|
144
|
+
return <Badge variant="secondary" className="bg-yellow-500 text-white">Idle</Badge>;
|
|
145
|
+
case 'ended':
|
|
146
|
+
return <Badge variant="destructive" className="bg-gray-500 text-white">Ended</Badge>;
|
|
147
|
+
default:
|
|
148
|
+
return <Badge>{status}</Badge>;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<DashboardLayout>
|
|
154
|
+
<div className="p-4 md:p-6 lg:p-8">
|
|
155
|
+
<h1 className="text-2xl font-bold mb-4">Sessions</h1>
|
|
156
|
+
<Card>
|
|
157
|
+
<CardHeader>
|
|
158
|
+
<CardTitle>Manage and monitor all user sessions.</CardTitle>
|
|
159
|
+
<Tabs value={statusFilter} onValueChange={setStatusFilter} className="w-full pt-4">
|
|
160
|
+
<TabsList>
|
|
161
|
+
<TabsTrigger value="all">All</TabsTrigger>
|
|
162
|
+
<TabsTrigger value="active">Active</TabsTrigger>
|
|
163
|
+
<TabsTrigger value="idle">Idle</TabsTrigger>
|
|
164
|
+
<TabsTrigger value="ended">Ended</TabsTrigger>
|
|
165
|
+
</TabsList>
|
|
166
|
+
</Tabs>
|
|
167
|
+
</CardHeader>
|
|
168
|
+
<CardContent>
|
|
169
|
+
{isLoading ? (
|
|
170
|
+
<div className="flex justify-center items-center h-64">
|
|
171
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
172
|
+
</div>
|
|
173
|
+
) : error ? (
|
|
174
|
+
<div className="text-center text-destructive py-10">{error}</div>
|
|
175
|
+
) : filteredSessions.length === 0 ? (
|
|
176
|
+
<div className="text-center text-muted-foreground py-10">
|
|
177
|
+
<p className="text-lg">No sessions found.</p>
|
|
178
|
+
<p>Start a new chat to create a session.</p>
|
|
179
|
+
</div>
|
|
180
|
+
) : (
|
|
181
|
+
<div className="overflow-x-auto">
|
|
182
|
+
<Table>
|
|
183
|
+
<TableHeader>
|
|
184
|
+
<TableRow>
|
|
185
|
+
<TableHead>Session ID</TableHead>
|
|
186
|
+
<TableHead>Agent</TableHead>
|
|
187
|
+
<TableHead>Status</TableHead>
|
|
188
|
+
<TableHead>Started</TableHead>
|
|
189
|
+
<TableHead>Duration</TableHead>
|
|
190
|
+
<TableHead className="text-right">Messages</TableHead>
|
|
191
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
192
|
+
</TableRow>
|
|
193
|
+
</TableHeader>
|
|
194
|
+
<TableBody>
|
|
195
|
+
{filteredSessions.map(session => (
|
|
196
|
+
<TableRow key={session._id} onClick={() => setSelectedSession(session)} className="cursor-pointer hover:bg-muted/50">
|
|
197
|
+
<TableCell className="font-mono text-xs">{session._id}</TableCell>
|
|
198
|
+
<TableCell>{session.agentId}</TableCell>
|
|
199
|
+
<TableCell>{statusBadge(session.status)}</TableCell>
|
|
200
|
+
<TableCell>{new Date(session._creationTime).toLocaleString()}</TableCell>
|
|
201
|
+
<TableCell>{formatDuration(session._creationTime, session.endTime)}</TableCell>
|
|
202
|
+
<TableCell className="text-right">{session.messageCount}</TableCell>
|
|
203
|
+
<TableCell className="text-right">
|
|
204
|
+
{session.status !== 'ended' && (
|
|
205
|
+
<Button
|
|
206
|
+
variant="destructive"
|
|
207
|
+
size="sm"
|
|
208
|
+
onClick={(e) => { e.stopPropagation(); handleEndSession(session._id); }}
|
|
209
|
+
>
|
|
210
|
+
<XCircle className="h-4 w-4 mr-2" />
|
|
211
|
+
End Session
|
|
212
|
+
</Button>
|
|
213
|
+
)}
|
|
214
|
+
</TableCell>
|
|
215
|
+
</TableRow>
|
|
216
|
+
))}
|
|
217
|
+
</TableBody>
|
|
218
|
+
</Table>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</CardContent>
|
|
222
|
+
</Card>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<Sheet open={!!selectedSession} onOpenChange={(open) => !open && setSelectedSession(null)}>
|
|
226
|
+
<SheetContent className="w-full sm:max-w-lg bg-background border-l">
|
|
227
|
+
{selectedSession && (
|
|
228
|
+
<>
|
|
229
|
+
<SheetHeader>
|
|
230
|
+
<SheetTitle>Session Details</SheetTitle>
|
|
231
|
+
<SheetDescription className="font-mono text-xs pt-2">{selectedSession._id}</SheetDescription>
|
|
232
|
+
</SheetHeader>
|
|
233
|
+
<div className="py-6 space-y-4">
|
|
234
|
+
<div className="flex items-center justify-between">
|
|
235
|
+
<span className="text-muted-foreground">Status</span>
|
|
236
|
+
{statusBadge(selectedSession.status)}
|
|
237
|
+
</div>
|
|
238
|
+
<div className="flex items-center justify-between">
|
|
239
|
+
<span className="text-muted-foreground flex items-center"><Activity className="h-4 w-4 mr-2" />Agent</span>
|
|
240
|
+
<span>{selectedSession.agentId}</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="flex items-center justify-between">
|
|
243
|
+
<span className="text-muted-foreground flex items-center"><Clock className="h-4 w-4 mr-2" />Started</span>
|
|
244
|
+
<span>{new Date(selectedSession._creationTime).toLocaleString()}</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex items-center justify-between">
|
|
247
|
+
<span className="text-muted-foreground flex items-center"><Clock className="h-4 w-4 mr-2" />Duration</span>
|
|
248
|
+
<span>{formatDuration(selectedSession._creationTime, selectedSession.endTime)}</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex items-center justify-between">
|
|
251
|
+
<span className="text-muted-foreground flex items-center"><MessageSquare className="h-4 w-4 mr-2" />Messages</span>
|
|
252
|
+
<span>{selectedSession.messageCount}</span>
|
|
253
|
+
</div>
|
|
254
|
+
{selectedSession.status === 'ended' && selectedSession.endTime && (
|
|
255
|
+
<div className="flex items-center justify-between">
|
|
256
|
+
<span className="text-muted-foreground flex items-center"><XCircle className="h-4 w-4 mr-2" />Ended</span>
|
|
257
|
+
<span>{new Date(selectedSession.endTime).toLocaleString()}</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
<SheetFooter>
|
|
262
|
+
<SheetClose asChild>
|
|
263
|
+
<Button variant="outline">Close</Button>
|
|
264
|
+
</SheetClose>
|
|
265
|
+
</SheetFooter>
|
|
266
|
+
</>
|
|
267
|
+
)}
|
|
268
|
+
</SheetContent>
|
|
269
|
+
</Sheet>
|
|
270
|
+
</DashboardLayout>
|
|
271
|
+
);
|
|
272
|
+
}
|