@agentforge-ai/cli 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/default/.env.example +46 -6
  2. package/dist/default/README.md +89 -9
  3. package/dist/default/convex/schema.ts +248 -4
  4. package/dist/default/dashboard/app/components/DashboardLayout.tsx +245 -0
  5. package/dist/default/dashboard/app/components/ui/badge.tsx +26 -0
  6. package/dist/default/dashboard/app/components/ui/button.tsx +41 -0
  7. package/dist/default/dashboard/app/components/ui/card.tsx +44 -0
  8. package/dist/default/dashboard/app/components/ui/dialog.tsx +66 -0
  9. package/dist/default/dashboard/app/components/ui/input.tsx +21 -0
  10. package/dist/default/dashboard/app/components/ui/label.tsx +18 -0
  11. package/dist/default/dashboard/app/components/ui/select.tsx +75 -0
  12. package/dist/default/dashboard/app/components/ui/sheet.tsx +73 -0
  13. package/dist/default/dashboard/app/components/ui/switch.tsx +34 -0
  14. package/dist/default/dashboard/app/components/ui/table.tsx +60 -0
  15. package/dist/default/dashboard/app/components/ui/tabs.tsx +50 -0
  16. package/dist/default/dashboard/app/components/ui/tooltip.tsx +23 -0
  17. package/dist/default/dashboard/app/lib/utils.ts +6 -0
  18. package/dist/default/dashboard/app/main.tsx +35 -0
  19. package/dist/default/dashboard/app/routeTree.gen.ts +352 -0
  20. package/dist/default/dashboard/app/routes/__root.tsx +10 -0
  21. package/dist/default/dashboard/app/routes/agents.tsx +255 -0
  22. package/dist/default/dashboard/app/routes/chat.tsx +427 -0
  23. package/dist/default/dashboard/app/routes/connections.tsx +413 -0
  24. package/dist/default/dashboard/app/routes/cron.tsx +322 -0
  25. package/dist/default/dashboard/app/routes/files.tsx +203 -0
  26. package/dist/default/dashboard/app/routes/index.tsx +141 -0
  27. package/dist/default/dashboard/app/routes/projects.tsx +254 -0
  28. package/dist/default/dashboard/app/routes/sessions.tsx +272 -0
  29. package/dist/default/dashboard/app/routes/settings.tsx +583 -0
  30. package/dist/default/dashboard/app/routes/skills.tsx +252 -0
  31. package/dist/default/dashboard/app/routes/usage.tsx +181 -0
  32. package/dist/default/dashboard/app/styles/globals.css +93 -0
  33. package/dist/default/dashboard/index.html +13 -0
  34. package/dist/default/dashboard/package.json +36 -0
  35. package/dist/default/dashboard/postcss.config.js +6 -0
  36. package/dist/default/dashboard/tailwind.config.js +50 -0
  37. package/dist/default/dashboard/tsconfig.json +24 -0
  38. package/dist/default/dashboard/vite.config.ts +16 -0
  39. package/dist/default/package.json +8 -3
  40. package/dist/default/skills/skill-creator/SKILL.md +270 -0
  41. package/dist/default/skills/skill-creator/config.json +11 -0
  42. package/dist/default/skills/skill-creator/index.ts +392 -0
  43. package/dist/default/src/agent.ts +85 -5
  44. package/dist/index.js +1574 -10
  45. package/dist/index.js.map +1 -1
  46. package/package.json +2 -1
  47. package/templates/default/.env.example +46 -6
  48. package/templates/default/README.md +89 -9
  49. package/templates/default/convex/schema.ts +248 -4
  50. package/templates/default/dashboard/app/components/DashboardLayout.tsx +245 -0
  51. package/templates/default/dashboard/app/components/ui/badge.tsx +26 -0
  52. package/templates/default/dashboard/app/components/ui/button.tsx +41 -0
  53. package/templates/default/dashboard/app/components/ui/card.tsx +44 -0
  54. package/templates/default/dashboard/app/components/ui/dialog.tsx +66 -0
  55. package/templates/default/dashboard/app/components/ui/input.tsx +21 -0
  56. package/templates/default/dashboard/app/components/ui/label.tsx +18 -0
  57. package/templates/default/dashboard/app/components/ui/select.tsx +75 -0
  58. package/templates/default/dashboard/app/components/ui/sheet.tsx +73 -0
  59. package/templates/default/dashboard/app/components/ui/switch.tsx +34 -0
  60. package/templates/default/dashboard/app/components/ui/table.tsx +60 -0
  61. package/templates/default/dashboard/app/components/ui/tabs.tsx +50 -0
  62. package/templates/default/dashboard/app/components/ui/tooltip.tsx +23 -0
  63. package/templates/default/dashboard/app/lib/utils.ts +6 -0
  64. package/templates/default/dashboard/app/main.tsx +35 -0
  65. package/templates/default/dashboard/app/routeTree.gen.ts +352 -0
  66. package/templates/default/dashboard/app/routes/__root.tsx +10 -0
  67. package/templates/default/dashboard/app/routes/agents.tsx +255 -0
  68. package/templates/default/dashboard/app/routes/chat.tsx +427 -0
  69. package/templates/default/dashboard/app/routes/connections.tsx +413 -0
  70. package/templates/default/dashboard/app/routes/cron.tsx +322 -0
  71. package/templates/default/dashboard/app/routes/files.tsx +203 -0
  72. package/templates/default/dashboard/app/routes/index.tsx +141 -0
  73. package/templates/default/dashboard/app/routes/projects.tsx +254 -0
  74. package/templates/default/dashboard/app/routes/sessions.tsx +272 -0
  75. package/templates/default/dashboard/app/routes/settings.tsx +583 -0
  76. package/templates/default/dashboard/app/routes/skills.tsx +252 -0
  77. package/templates/default/dashboard/app/routes/usage.tsx +181 -0
  78. package/templates/default/dashboard/app/styles/globals.css +93 -0
  79. package/templates/default/dashboard/index.html +13 -0
  80. package/templates/default/dashboard/package.json +36 -0
  81. package/templates/default/dashboard/postcss.config.js +6 -0
  82. package/templates/default/dashboard/tailwind.config.js +50 -0
  83. package/templates/default/dashboard/tsconfig.json +24 -0
  84. package/templates/default/dashboard/vite.config.ts +16 -0
  85. package/templates/default/package.json +8 -3
  86. package/templates/default/skills/skill-creator/SKILL.md +270 -0
  87. package/templates/default/skills/skill-creator/config.json +11 -0
  88. package/templates/default/skills/skill-creator/index.ts +392 -0
  89. package/templates/default/src/agent.ts +85 -5
@@ -0,0 +1,322 @@
1
+ import { createFileRoute } from '@tanstack/react-router';
2
+ import { DashboardLayout } from '../components/DashboardLayout';
3
+ import { useState } from 'react';
4
+ import { Button } from '../components/ui/button';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
6
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '../components/ui/dialog';
7
+ import { Input } from '../components/ui/input';
8
+ import { Label } from '../components/ui/label';
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
10
+ import { Switch } from '../components/ui/switch';
11
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../components/ui/table';
12
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../components/ui/tooltip';
13
+ import { Clock, Play, Pause, Plus, History, AlertCircle, Trash2, Edit } from 'lucide-react';
14
+ // import { useQuery, useMutation } from 'convex/react';
15
+ // import { api } from '../../convex/_generated/api';
16
+
17
+ export const Route = createFileRoute('/cron')({ component: CronPageComponent });
18
+
19
+ type CronJob = {
20
+ id: string;
21
+ name: string;
22
+ schedule: string;
23
+ scheduleReadable: string;
24
+ agent: string;
25
+ status: 'enabled' | 'disabled';
26
+ lastRun: string | null;
27
+ nextRun: string;
28
+ };
29
+
30
+ const initialCronJobs: CronJob[] = [
31
+ {
32
+ id: 'cron_1',
33
+ name: 'Daily Report',
34
+ schedule: '0 9 * * *',
35
+ scheduleReadable: 'Every day at 9:00 AM',
36
+ agent: 'reporting-agent',
37
+ status: 'enabled',
38
+ lastRun: '2026-02-15 09:00:12',
39
+ nextRun: '2026-02-16 09:00:00',
40
+ },
41
+ {
42
+ id: 'cron_2',
43
+ name: 'Hourly Sync',
44
+ schedule: '0 * * * *',
45
+ scheduleReadable: 'Every hour',
46
+ agent: 'sync-agent',
47
+ status: 'disabled',
48
+ lastRun: '2026-02-15 14:00:05',
49
+ nextRun: '2026-02-15 16:00:00',
50
+ },
51
+ {
52
+ id: 'cron_3',
53
+ name: 'Nightly Cleanup',
54
+ schedule: '0 2 * * *',
55
+ scheduleReadable: 'Every day at 2:00 AM',
56
+ agent: 'cleanup-agent',
57
+ status: 'enabled',
58
+ lastRun: '2026-02-15 02:00:08',
59
+ nextRun: '2026-02-16 02:00:00',
60
+ },
61
+ ];
62
+
63
+ function CronPageComponent() {
64
+ // const cronJobs = useQuery(api.cronJobs.list) ?? [];
65
+ // const createCronJob = useMutation(api.cronJobs.create);
66
+ // const updateCronJob = useMutation(api.cronJobs.update);
67
+ // const deleteCronJob = useMutation(api.cronJobs.delete);
68
+
69
+ const [cronJobs, setCronJobs] = useState<CronJob[]>(initialCronJobs);
70
+ const [isModalOpen, setIsModalOpen] = useState(false);
71
+ const [editingJob, setEditingJob] = useState<CronJob | null>(null);
72
+ const [selectedJobHistory, setSelectedJobHistory] = useState<CronJob | null>(null);
73
+
74
+ const handleSaveJob = (jobData: Omit<CronJob, 'id'>) => {
75
+ if (editingJob) {
76
+ const updatedJob = { ...editingJob, ...jobData };
77
+ setCronJobs(cronJobs.map(j => j.id === editingJob.id ? updatedJob : j));
78
+ // updateCronJob({ id: updatedJob.id, ...jobData });
79
+ } else {
80
+ const newJob = { ...jobData, id: `cron_${Date.now()}` };
81
+ setCronJobs([...cronJobs, newJob]);
82
+ // createCronJob(jobData);
83
+ }
84
+ setIsModalOpen(false);
85
+ setEditingJob(null);
86
+ };
87
+
88
+ const handleToggleStatus = (job: CronJob) => {
89
+ const newStatus = job.status === 'enabled' ? 'disabled' : 'enabled';
90
+ const updatedJob = { ...job, status: newStatus };
91
+ setCronJobs(cronJobs.map(j => j.id === job.id ? updatedJob : j));
92
+ // updateCronJob({ id: job.id, status: newStatus });
93
+ };
94
+
95
+ const handleDeleteJob = (id: string) => {
96
+ if (window.confirm('Are you sure you want to delete this cron job?')) {
97
+ setCronJobs(cronJobs.filter(j => j.id !== id));
98
+ // deleteCronJob({ id });
99
+ }
100
+ };
101
+
102
+ const openEditModal = (job: CronJob) => {
103
+ setEditingJob(job);
104
+ setIsModalOpen(true);
105
+ };
106
+
107
+ const openCreateModal = () => {
108
+ setEditingJob(null);
109
+ setIsModalOpen(true);
110
+ };
111
+
112
+ return (
113
+ <DashboardLayout>
114
+ <div className="p-6">
115
+ <div className="flex justify-between items-center mb-6">
116
+ <h1 className="text-3xl font-bold">Cron Jobs</h1>
117
+ <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
118
+ <DialogTrigger asChild>
119
+ <Button onClick={openCreateModal}>
120
+ <Plus className="mr-2 h-4 w-4" /> Add Cron Job
121
+ </Button>
122
+ </DialogTrigger>
123
+ <CronJobForm
124
+ job={editingJob}
125
+ onSave={handleSaveJob}
126
+ onClose={() => setIsModalOpen(false)}
127
+ />
128
+ </Dialog>
129
+ </div>
130
+
131
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
132
+ <Card className="lg:col-span-2">
133
+ <CardHeader>
134
+ <CardTitle>Scheduled Jobs</CardTitle>
135
+ </CardHeader>
136
+ <CardContent>
137
+ {cronJobs.length > 0 ? (
138
+ <Table>
139
+ <TableHeader>
140
+ <TableRow>
141
+ <TableHead>Status</TableHead>
142
+ <TableHead>Name</TableHead>
143
+ <TableHead>Schedule</TableHead>
144
+ <TableHead>Agent</TableHead>
145
+ <TableHead>Last Run</TableHead>
146
+ <TableHead>Next Run</TableHead>
147
+ <TableHead className="text-right">Actions</TableHead>
148
+ </TableRow>
149
+ </TableHeader>
150
+ <TableBody>
151
+ {cronJobs.map(job => (
152
+ <TableRow key={job.id}>
153
+ <TableCell>
154
+ <TooltipProvider>
155
+ <Tooltip>
156
+ <TooltipTrigger asChild>
157
+ <button onClick={() => handleToggleStatus(job)}>
158
+ {job.status === 'enabled' ? (
159
+ <Play className="h-5 w-5 text-green-500" />
160
+ ) : (
161
+ <Pause className="h-5 w-5 text-yellow-500" />
162
+ )}
163
+ </button>
164
+ </TooltipTrigger>
165
+ <TooltipContent>
166
+ <p>{job.status === 'enabled' ? 'Enabled (Click to disable)' : 'Disabled (Click to enable)'}</p>
167
+ </TooltipContent>
168
+ </Tooltip>
169
+ </TooltipProvider>
170
+ </TableCell>
171
+ <TableCell className="font-medium">{job.name}</TableCell>
172
+ <TableCell>
173
+ <TooltipProvider>
174
+ <Tooltip>
175
+ <TooltipTrigger className="cursor-help">{job.scheduleReadable}</TooltipTrigger>
176
+ <TooltipContent><p>{job.schedule}</p></TooltipContent>
177
+ </Tooltip>
178
+ </TooltipProvider>
179
+ </TableCell>
180
+ <TableCell>{job.agent}</TableCell>
181
+ <TableCell>{job.lastRun || 'N/A'}</TableCell>
182
+ <TableCell>{job.nextRun}</TableCell>
183
+ <TableCell className="text-right space-x-2">
184
+ <Button variant="ghost" size="icon" onClick={() => setSelectedJobHistory(job)}>
185
+ <History className="h-4 w-4" />
186
+ </Button>
187
+ <Button variant="ghost" size="icon" onClick={() => openEditModal(job)}>
188
+ <Edit className="h-4 w-4" />
189
+ </Button>
190
+ <Button variant="destructive" size="icon" onClick={() => handleDeleteJob(job.id)}>
191
+ <Trash2 className="h-4 w-4" />
192
+ </Button>
193
+ </TableCell>
194
+ </TableRow>
195
+ ))}
196
+ </TableBody>
197
+ </Table>
198
+ ) : (
199
+ <div className="text-center py-12 text-muted-foreground">
200
+ <Clock className="mx-auto h-12 w-12" />
201
+ <h3 className="mt-4 text-lg font-semibold">No Cron Jobs Scheduled</h3>
202
+ <p className="mt-2 text-sm">Get started by adding a new cron job.</p>
203
+ </div>
204
+ )}
205
+ </CardContent>
206
+ </Card>
207
+
208
+ <Card className="lg:col-span-1 h-fit">
209
+ <CardHeader>
210
+ <CardTitle className="flex items-center">
211
+ <History className="mr-2 h-5 w-5" />
212
+ Execution History
213
+ </CardTitle>
214
+ </CardHeader>
215
+ <CardContent>
216
+ {selectedJobHistory ? (
217
+ <div>
218
+ <h4 className="font-semibold mb-2">{selectedJobHistory.name}</h4>
219
+ <ul className="space-y-2 text-sm text-muted-foreground">
220
+ {[...Array(5)].map((_, i) => (
221
+ <li key={i} className="flex items-center justify-between p-2 bg-background rounded-md border">
222
+ <span>Run #{15 - i}</span>
223
+ <span className="text-xs">{(new Date(Date.now() - i * 3600000)).toLocaleString()}</span>
224
+ <span className="text-green-500">Success</span>
225
+ </li>
226
+ ))}
227
+ </ul>
228
+ </div>
229
+ ) : (
230
+ <div className="text-center py-10 text-muted-foreground">
231
+ <AlertCircle className="mx-auto h-10 w-10" />
232
+ <p className="mt-4 text-sm">Select a job to view its execution history.</p>
233
+ </div>
234
+ )}
235
+ </CardContent>
236
+ </Card>
237
+ </div>
238
+ </div>
239
+ </DashboardLayout>
240
+ );
241
+ }
242
+
243
+ interface CronJobFormProps {
244
+ job: CronJob | null;
245
+ onSave: (jobData: Omit<CronJob, 'id'>) => void;
246
+ onClose: () => void;
247
+ }
248
+
249
+ function CronJobForm({ job, onSave, onClose }: CronJobFormProps) {
250
+ const [name, setName] = useState(job?.name || '');
251
+ const [agent, setAgent] = useState(job?.agent || '');
252
+ const [schedulePreset, setSchedulePreset] = useState('custom');
253
+ const [customSchedule, setCustomSchedule] = useState(job?.schedule || '');
254
+
255
+ const handleSubmit = (e: React.FormEvent) => {
256
+ e.preventDefault();
257
+ const schedule = schedulePreset === 'custom' ? customSchedule : schedulePreset;
258
+ // This is a simplified readable schedule. A real implementation would use a library.
259
+ const scheduleReadable = schedulePreset === 'custom' ? `Custom: ${customSchedule}` : `Every ${schedulePreset.split(' ')[1]}`;
260
+
261
+ onSave({
262
+ name,
263
+ agent,
264
+ schedule,
265
+ scheduleReadable,
266
+ status: job?.status || 'enabled',
267
+ lastRun: job?.lastRun || null,
268
+ nextRun: job?.nextRun || 'Calculating...',
269
+ });
270
+ };
271
+
272
+ return (
273
+ <DialogContent className="sm:max-w-[425px]">
274
+ <DialogHeader>
275
+ <DialogTitle>{job ? 'Edit Cron Job' : 'Create Cron Job'}</DialogTitle>
276
+ </DialogHeader>
277
+ <form onSubmit={handleSubmit}>
278
+ <div className="grid gap-4 py-4">
279
+ <div className="grid grid-cols-4 items-center gap-4">
280
+ <Label htmlFor="name" className="text-right">Name</Label>
281
+ <Input id="name" value={name} onChange={e => setName(e.target.value)} className="col-span-3" placeholder="e.g., Daily Summary" required />
282
+ </div>
283
+ <div className="grid grid-cols-4 items-center gap-4">
284
+ <Label htmlFor="agent" className="text-right">Agent</Label>
285
+ <Input id="agent" value={agent} onChange={e => setAgent(e.target.value)} className="col-span-3" placeholder="e.g., reporting-agent" required />
286
+ </div>
287
+ <div className="grid grid-cols-4 items-center gap-4">
288
+ <Label htmlFor="schedule" className="text-right">Schedule</Label>
289
+ <Select onValueChange={setSchedulePreset} defaultValue={schedulePreset}>
290
+ <SelectTrigger className="col-span-3">
291
+ <SelectValue placeholder="Select a preset" />
292
+ </SelectTrigger>
293
+ <SelectContent>
294
+ <SelectItem value="0 * * * *">Every Hour</SelectItem>
295
+ <SelectItem value="0 0 * * *">Every Day (midnight)</SelectItem>
296
+ <SelectItem value="0 0 * * 1">Every Week (Monday)</SelectItem>
297
+ <SelectItem value="custom">Custom Cron Expression</SelectItem>
298
+ </SelectContent>
299
+ </Select>
300
+ </div>
301
+ {schedulePreset === 'custom' && (
302
+ <div className="grid grid-cols-4 items-center gap-4">
303
+ <Label htmlFor="custom-schedule" className="text-right">Cron</Label>
304
+ <Input
305
+ id="custom-schedule"
306
+ value={customSchedule}
307
+ onChange={e => setCustomSchedule(e.target.value)}
308
+ className="col-span-3"
309
+ placeholder="* * * * *"
310
+ required
311
+ />
312
+ </div>
313
+ )}
314
+ </div>
315
+ <DialogFooter>
316
+ <Button type="button" variant="ghost" onClick={onClose}>Cancel</Button>
317
+ <Button type="submit">Save Job</Button>
318
+ </DialogFooter>
319
+ </form>
320
+ </DialogContent>
321
+ );
322
+ }
@@ -0,0 +1,203 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { DashboardLayout } from "../components/DashboardLayout";
3
+ import { useState, useRef } from "react";
4
+ import {
5
+ Folder, File, Upload, Trash2, Edit2, FolderPlus, Download,
6
+ ChevronRight, Search, Grid, List, X, FileText, FileImage, FileCode, FileArchive, Home,
7
+ } from "lucide-react";
8
+
9
+ export const Route = createFileRoute("/files")({ component: FilesPage });
10
+
11
+ interface FolderItem { id: string; name: string; parentId: string | null; createdAt: number; }
12
+ interface FileItem { id: string; name: string; folderId: string | null; size: number; type: string; createdAt: number; updatedAt: number; }
13
+
14
+ function formatFileSize(bytes: number): string {
15
+ if (bytes === 0) return "0 B";
16
+ const k = 1024, sizes = ["B", "KB", "MB", "GB"];
17
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
18
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
19
+ }
20
+ function formatDate(ts: number): string {
21
+ return new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
22
+ }
23
+ function getFileIcon(type: string) {
24
+ if (type.startsWith("image/")) return FileImage;
25
+ if (type.includes("script") || type.includes("json") || type.includes("html") || type.includes("css")) return FileCode;
26
+ if (type.includes("zip") || type.includes("tar") || type.includes("rar")) return FileArchive;
27
+ return FileText;
28
+ }
29
+
30
+ function FilesPage() {
31
+ const [folders, setFolders] = useState<FolderItem[]>([
32
+ { id: "f1", name: "Documents", parentId: null, createdAt: Date.now() - 86400000 * 5 },
33
+ { id: "f2", name: "Images", parentId: null, createdAt: Date.now() - 86400000 * 3 },
34
+ { id: "f3", name: "Agent Data", parentId: null, createdAt: Date.now() - 86400000 * 2 },
35
+ { id: "f4", name: "Reports", parentId: "f1", createdAt: Date.now() - 86400000 },
36
+ { id: "f5", name: "Exports", parentId: null, createdAt: Date.now() - 86400000 * 7 },
37
+ ]);
38
+ const [files, setFiles] = useState<FileItem[]>([
39
+ { id: "file1", name: "agent-config.json", folderId: null, size: 2048, type: "application/json", createdAt: Date.now() - 3600000, updatedAt: Date.now() - 3600000 },
40
+ { id: "file2", name: "training-data.csv", folderId: "f1", size: 1048576, type: "text/csv", createdAt: Date.now() - 7200000, updatedAt: Date.now() - 7200000 },
41
+ { id: "file3", name: "screenshot.png", folderId: "f2", size: 524288, type: "image/png", createdAt: Date.now() - 86400000, updatedAt: Date.now() - 86400000 },
42
+ { id: "file4", name: "report-q1.pdf", folderId: "f4", size: 3145728, type: "application/pdf", createdAt: Date.now() - 172800000, updatedAt: Date.now() - 172800000 },
43
+ { id: "file5", name: "workflow.ts", folderId: "f3", size: 4096, type: "text/typescript", createdAt: Date.now() - 259200000, updatedAt: Date.now() - 259200000 },
44
+ { id: "file6", name: "readme.md", folderId: null, size: 1024, type: "text/markdown", createdAt: Date.now() - 345600000, updatedAt: Date.now() - 345600000 },
45
+ ]);
46
+ const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
47
+ const [searchQuery, setSearchQuery] = useState("");
48
+ const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
49
+ const [showNewFolderModal, setShowNewFolderModal] = useState(false);
50
+ const [showRenameModal, setShowRenameModal] = useState(false);
51
+ const [renameTarget, setRenameTarget] = useState<{ id: string; name: string; type: "file" | "folder" } | null>(null);
52
+ const [newFolderName, setNewFolderName] = useState("");
53
+ const [renameName, setRenameName] = useState("");
54
+ const [isDragging, setIsDragging] = useState(false);
55
+ const fileInputRef = useRef<HTMLInputElement>(null);
56
+
57
+ const currentFolders = folders.filter((f) => f.parentId === currentFolderId);
58
+ const currentFiles = files.filter((f) => f.folderId === currentFolderId);
59
+ const filteredFolders = searchQuery ? currentFolders.filter((f) => f.name.toLowerCase().includes(searchQuery.toLowerCase())) : currentFolders;
60
+ const filteredFiles = searchQuery ? currentFiles.filter((f) => f.name.toLowerCase().includes(searchQuery.toLowerCase())) : currentFiles;
61
+
62
+ const breadcrumbs: { id: string | null; name: string }[] = [{ id: null, name: "Root" }];
63
+ let crumbId = currentFolderId;
64
+ const crumbParts: { id: string; name: string }[] = [];
65
+ while (crumbId) {
66
+ const folder = folders.find((f) => f.id === crumbId);
67
+ if (folder) { crumbParts.unshift({ id: folder.id, name: folder.name }); crumbId = folder.parentId; } else break;
68
+ }
69
+ breadcrumbs.push(...crumbParts);
70
+
71
+ const handleCreateFolder = () => {
72
+ if (!newFolderName.trim()) return;
73
+ setFolders([...folders, { id: `f_${Date.now()}`, name: newFolderName.trim(), parentId: currentFolderId, createdAt: Date.now() }]);
74
+ setNewFolderName(""); setShowNewFolderModal(false);
75
+ };
76
+ const handleDeleteFolder = (id: string) => { setFolders(folders.filter((f) => f.id !== id)); setFiles(files.filter((f) => f.folderId !== id)); };
77
+ const handleDeleteFile = (id: string) => { setFiles(files.filter((f) => f.id !== id)); };
78
+ const handleRename = () => {
79
+ if (!renameTarget || !renameName.trim()) return;
80
+ if (renameTarget.type === "folder") setFolders(folders.map((f) => (f.id === renameTarget.id ? { ...f, name: renameName.trim() } : f)));
81
+ else setFiles(files.map((f) => (f.id === renameTarget.id ? { ...f, name: renameName.trim(), updatedAt: Date.now() } : f)));
82
+ setShowRenameModal(false); setRenameTarget(null); setRenameName("");
83
+ };
84
+ const openRename = (id: string, name: string, type: "file" | "folder") => { setRenameTarget({ id, name, type }); setRenameName(name); setShowRenameModal(true); };
85
+ const handleFileDrop = (e: React.DragEvent) => {
86
+ e.preventDefault(); setIsDragging(false);
87
+ const newFiles: FileItem[] = Array.from(e.dataTransfer.files).map((f) => ({
88
+ id: `file_${Date.now()}_${Math.random().toString(36).slice(2)}`, name: f.name, folderId: currentFolderId,
89
+ size: f.size, type: f.type || "application/octet-stream", createdAt: Date.now(), updatedAt: Date.now(),
90
+ }));
91
+ setFiles([...files, ...newFiles]);
92
+ };
93
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
94
+ if (!e.target.files) return;
95
+ const newFiles: FileItem[] = Array.from(e.target.files).map((f) => ({
96
+ id: `file_${Date.now()}_${Math.random().toString(36).slice(2)}`, name: f.name, folderId: currentFolderId,
97
+ size: f.size, type: f.type || "application/octet-stream", createdAt: Date.now(), updatedAt: Date.now(),
98
+ }));
99
+ setFiles([...files, ...newFiles]);
100
+ if (fileInputRef.current) fileInputRef.current.value = "";
101
+ };
102
+
103
+ return (
104
+ <DashboardLayout>
105
+ <div className="space-y-6">
106
+ <div className="flex items-center justify-between">
107
+ <div><h1 className="text-3xl font-bold">Files</h1><p className="text-muted-foreground mt-1">Manage files and folders for your agents</p></div>
108
+ <div className="flex items-center gap-2">
109
+ <button onClick={() => setShowNewFolderModal(true)} className="flex items-center gap-2 px-3 py-2 bg-card border rounded-lg hover:bg-accent transition-colors"><FolderPlus className="h-4 w-4" /><span className="text-sm">New Folder</span></button>
110
+ <button onClick={() => fileInputRef.current?.click()} className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"><Upload className="h-4 w-4" /><span className="text-sm">Upload</span></button>
111
+ <input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileUpload} />
112
+ </div>
113
+ </div>
114
+ <div className="flex items-center justify-between">
115
+ <div className="flex items-center gap-1 text-sm">
116
+ {breadcrumbs.map((crumb, i) => (
117
+ <div key={crumb.id ?? "root"} className="flex items-center gap-1">
118
+ {i > 0 && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
119
+ <button onClick={() => setCurrentFolderId(crumb.id)} className={`hover:text-primary transition-colors ${i === breadcrumbs.length - 1 ? "text-foreground font-medium" : "text-muted-foreground"}`}>
120
+ {i === 0 ? <Home className="h-4 w-4" /> : crumb.name}
121
+ </button>
122
+ </div>
123
+ ))}
124
+ </div>
125
+ <div className="flex items-center gap-2">
126
+ <div className="relative"><Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /><input type="text" placeholder="Search files..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-9 pr-3 py-1.5 bg-card border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary w-48" /></div>
127
+ <div className="flex items-center bg-card border rounded-lg">
128
+ <button onClick={() => setViewMode("grid")} className={`p-1.5 rounded-l-lg ${viewMode === "grid" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`}><Grid className="h-4 w-4" /></button>
129
+ <button onClick={() => setViewMode("list")} className={`p-1.5 rounded-r-lg ${viewMode === "list" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`}><List className="h-4 w-4" /></button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ <div onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={handleFileDrop} className={`min-h-[400px] rounded-lg border-2 border-dashed transition-colors ${isDragging ? "border-primary bg-primary/5" : "border-transparent"}`}>
134
+ {filteredFolders.length === 0 && filteredFiles.length === 0 ? (
135
+ <div className="flex flex-col items-center justify-center h-[400px] text-center">
136
+ <Upload className="h-12 w-12 text-muted-foreground mb-4" /><h3 className="text-lg font-medium mb-1">No files here</h3>
137
+ <p className="text-muted-foreground text-sm mb-4">Drag and drop files or click Upload to get started</p>
138
+ <button onClick={() => fileInputRef.current?.click()} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 text-sm">Upload Files</button>
139
+ </div>
140
+ ) : viewMode === "grid" ? (
141
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3">
142
+ {filteredFolders.map((folder) => (
143
+ <div key={folder.id} className="group bg-card border rounded-lg p-4 hover:border-primary/50 cursor-pointer transition-colors relative" onDoubleClick={() => setCurrentFolderId(folder.id)}>
144
+ <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
145
+ <button onClick={(e) => { e.stopPropagation(); openRename(folder.id, folder.name, "folder"); }} className="p-1 hover:bg-accent rounded"><Edit2 className="h-3 w-3" /></button>
146
+ <button onClick={(e) => { e.stopPropagation(); handleDeleteFolder(folder.id); }} className="p-1 hover:bg-destructive/20 rounded text-destructive"><Trash2 className="h-3 w-3" /></button>
147
+ </div>
148
+ <Folder className="h-10 w-10 text-blue-400 mb-2" /><p className="text-sm font-medium truncate">{folder.name}</p><p className="text-xs text-muted-foreground mt-1">{formatDate(folder.createdAt)}</p>
149
+ </div>
150
+ ))}
151
+ {filteredFiles.map((file) => { const Icon = getFileIcon(file.type); return (
152
+ <div key={file.id} className="group bg-card border rounded-lg p-4 hover:border-primary/50 cursor-pointer transition-colors relative">
153
+ <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
154
+ <button onClick={() => openRename(file.id, file.name, "file")} className="p-1 hover:bg-accent rounded"><Edit2 className="h-3 w-3" /></button>
155
+ <button className="p-1 hover:bg-accent rounded"><Download className="h-3 w-3" /></button>
156
+ <button onClick={() => handleDeleteFile(file.id)} className="p-1 hover:bg-destructive/20 rounded text-destructive"><Trash2 className="h-3 w-3" /></button>
157
+ </div>
158
+ <Icon className="h-10 w-10 text-muted-foreground mb-2" /><p className="text-sm font-medium truncate">{file.name}</p><p className="text-xs text-muted-foreground mt-1">{formatFileSize(file.size)}</p>
159
+ </div>
160
+ ); })}
161
+ </div>
162
+ ) : (
163
+ <div className="bg-card border rounded-lg overflow-hidden">
164
+ <table className="w-full text-sm">
165
+ <thead><tr className="border-b text-muted-foreground text-left"><th className="px-4 py-3 font-medium">Name</th><th className="px-4 py-3 font-medium">Type</th><th className="px-4 py-3 font-medium">Size</th><th className="px-4 py-3 font-medium">Modified</th><th className="px-4 py-3 font-medium w-24">Actions</th></tr></thead>
166
+ <tbody>
167
+ {filteredFolders.map((folder) => (
168
+ <tr key={folder.id} className="border-b hover:bg-accent/50 cursor-pointer" onDoubleClick={() => setCurrentFolderId(folder.id)}>
169
+ <td className="px-4 py-3 flex items-center gap-2"><Folder className="h-4 w-4 text-blue-400" /><span className="font-medium">{folder.name}</span></td>
170
+ <td className="px-4 py-3 text-muted-foreground">Folder</td><td className="px-4 py-3 text-muted-foreground">—</td><td className="px-4 py-3 text-muted-foreground">{formatDate(folder.createdAt)}</td>
171
+ <td className="px-4 py-3"><div className="flex items-center gap-1"><button onClick={() => openRename(folder.id, folder.name, "folder")} className="p-1 hover:bg-accent rounded"><Edit2 className="h-3 w-3" /></button><button onClick={() => handleDeleteFolder(folder.id)} className="p-1 hover:bg-destructive/20 rounded text-destructive"><Trash2 className="h-3 w-3" /></button></div></td>
172
+ </tr>
173
+ ))}
174
+ {filteredFiles.map((file) => { const Icon = getFileIcon(file.type); return (
175
+ <tr key={file.id} className="border-b hover:bg-accent/50">
176
+ <td className="px-4 py-3 flex items-center gap-2"><Icon className="h-4 w-4 text-muted-foreground" /><span>{file.name}</span></td>
177
+ <td className="px-4 py-3 text-muted-foreground">{file.type.split("/").pop()}</td><td className="px-4 py-3 text-muted-foreground">{formatFileSize(file.size)}</td><td className="px-4 py-3 text-muted-foreground">{formatDate(file.updatedAt)}</td>
178
+ <td className="px-4 py-3"><div className="flex items-center gap-1"><button onClick={() => openRename(file.id, file.name, "file")} className="p-1 hover:bg-accent rounded"><Edit2 className="h-3 w-3" /></button><button className="p-1 hover:bg-accent rounded"><Download className="h-3 w-3" /></button><button onClick={() => handleDeleteFile(file.id)} className="p-1 hover:bg-destructive/20 rounded text-destructive"><Trash2 className="h-3 w-3" /></button></div></td>
179
+ </tr>
180
+ ); })}
181
+ </tbody>
182
+ </table>
183
+ </div>
184
+ )}
185
+ </div>
186
+ {showNewFolderModal && (
187
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"><div className="bg-card border rounded-lg p-6 w-full max-w-md">
188
+ <div className="flex items-center justify-between mb-4"><h2 className="text-lg font-semibold">New Folder</h2><button onClick={() => setShowNewFolderModal(false)} className="p-1 hover:bg-accent rounded"><X className="h-4 w-4" /></button></div>
189
+ <input type="text" placeholder="Folder name" value={newFolderName} onChange={(e) => setNewFolderName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()} className="w-full px-3 py-2 bg-background border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary mb-4" autoFocus />
190
+ <div className="flex justify-end gap-2"><button onClick={() => setShowNewFolderModal(false)} className="px-4 py-2 text-sm border rounded-lg hover:bg-accent">Cancel</button><button onClick={handleCreateFolder} className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90">Create</button></div>
191
+ </div></div>
192
+ )}
193
+ {showRenameModal && renameTarget && (
194
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"><div className="bg-card border rounded-lg p-6 w-full max-w-md">
195
+ <div className="flex items-center justify-between mb-4"><h2 className="text-lg font-semibold">Rename {renameTarget.type}</h2><button onClick={() => setShowRenameModal(false)} className="p-1 hover:bg-accent rounded"><X className="h-4 w-4" /></button></div>
196
+ <input type="text" value={renameName} onChange={(e) => setRenameName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleRename()} className="w-full px-3 py-2 bg-background border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary mb-4" autoFocus />
197
+ <div className="flex justify-end gap-2"><button onClick={() => setShowRenameModal(false)} className="px-4 py-2 text-sm border rounded-lg hover:bg-accent">Cancel</button><button onClick={handleRename} className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90">Rename</button></div>
198
+ </div></div>
199
+ )}
200
+ </div>
201
+ </DashboardLayout>
202
+ );
203
+ }