@agentforge-ai/cli 0.4.3 → 0.5.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 (67) hide show
  1. package/dist/default/convex/agents.ts +204 -0
  2. package/dist/default/convex/apiKeys.ts +133 -0
  3. package/dist/default/convex/cronJobs.ts +224 -0
  4. package/dist/default/convex/files.ts +103 -0
  5. package/dist/default/convex/folders.ts +110 -0
  6. package/dist/default/convex/heartbeat.ts +371 -0
  7. package/dist/default/convex/logs.ts +66 -0
  8. package/dist/default/convex/mastraIntegration.ts +185 -0
  9. package/dist/default/convex/mcpConnections.ts +127 -0
  10. package/dist/default/convex/messages.ts +90 -0
  11. package/dist/default/convex/projects.ts +114 -0
  12. package/dist/default/convex/schema.ts +150 -83
  13. package/dist/default/convex/sessions.ts +174 -0
  14. package/dist/default/convex/settings.ts +79 -0
  15. package/dist/default/convex/skills.ts +178 -0
  16. package/dist/default/convex/threads.ts +100 -0
  17. package/dist/default/convex/usage.ts +195 -0
  18. package/dist/default/convex/vault.ts +397 -0
  19. package/dist/default/dashboard/app/main.tsx +7 -3
  20. package/dist/default/dashboard/app/routes/agents.tsx +103 -161
  21. package/dist/default/dashboard/app/routes/chat.tsx +163 -317
  22. package/dist/default/dashboard/app/routes/connections.tsx +247 -386
  23. package/dist/default/dashboard/app/routes/cron.tsx +127 -286
  24. package/dist/default/dashboard/app/routes/files.tsx +184 -167
  25. package/dist/default/dashboard/app/routes/index.tsx +63 -96
  26. package/dist/default/dashboard/app/routes/projects.tsx +106 -225
  27. package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
  28. package/dist/default/dashboard/app/routes/settings.tsx +316 -532
  29. package/dist/default/dashboard/app/routes/skills.tsx +329 -216
  30. package/dist/default/dashboard/app/routes/usage.tsx +107 -150
  31. package/dist/default/dashboard/tsconfig.json +3 -2
  32. package/dist/default/dashboard/vite.config.ts +6 -0
  33. package/dist/index.js +256 -49
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/default/convex/agents.ts +204 -0
  37. package/templates/default/convex/apiKeys.ts +133 -0
  38. package/templates/default/convex/cronJobs.ts +224 -0
  39. package/templates/default/convex/files.ts +103 -0
  40. package/templates/default/convex/folders.ts +110 -0
  41. package/templates/default/convex/heartbeat.ts +371 -0
  42. package/templates/default/convex/logs.ts +66 -0
  43. package/templates/default/convex/mastraIntegration.ts +185 -0
  44. package/templates/default/convex/mcpConnections.ts +127 -0
  45. package/templates/default/convex/messages.ts +90 -0
  46. package/templates/default/convex/projects.ts +114 -0
  47. package/templates/default/convex/schema.ts +150 -83
  48. package/templates/default/convex/sessions.ts +174 -0
  49. package/templates/default/convex/settings.ts +79 -0
  50. package/templates/default/convex/skills.ts +178 -0
  51. package/templates/default/convex/threads.ts +100 -0
  52. package/templates/default/convex/usage.ts +195 -0
  53. package/templates/default/convex/vault.ts +397 -0
  54. package/templates/default/dashboard/app/main.tsx +7 -3
  55. package/templates/default/dashboard/app/routes/agents.tsx +103 -161
  56. package/templates/default/dashboard/app/routes/chat.tsx +163 -317
  57. package/templates/default/dashboard/app/routes/connections.tsx +247 -386
  58. package/templates/default/dashboard/app/routes/cron.tsx +127 -286
  59. package/templates/default/dashboard/app/routes/files.tsx +184 -167
  60. package/templates/default/dashboard/app/routes/index.tsx +63 -96
  61. package/templates/default/dashboard/app/routes/projects.tsx +106 -225
  62. package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
  63. package/templates/default/dashboard/app/routes/settings.tsx +316 -532
  64. package/templates/default/dashboard/app/routes/skills.tsx +329 -216
  65. package/templates/default/dashboard/app/routes/usage.tsx +107 -150
  66. package/templates/default/dashboard/tsconfig.json +3 -2
  67. package/templates/default/dashboard/vite.config.ts +6 -0
@@ -1,201 +1,218 @@
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";
1
+ import { createFileRoute } from '@tanstack/react-router';
2
+ import { DashboardLayout } from '../components/DashboardLayout';
3
+ import { useState, useMemo } from 'react';
4
+ import { useQuery, useMutation } from 'convex/react';
5
+ import { api } from '@convex/_generated/api';
6
+ import { Folder, File, Upload, Trash2, FolderPlus, Search, Grid, List, Home, FileText, FileImage, FileCode, FileArchive, X, ChevronRight } from 'lucide-react';
8
7
 
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; }
8
+ export const Route = createFileRoute('/files')({ component: FilesPage });
13
9
 
14
10
  function formatFileSize(bytes: number): string {
15
- if (bytes === 0) return "0 B";
16
- const k = 1024, sizes = ["B", "KB", "MB", "GB"];
11
+ if (bytes === 0) return '0 B';
12
+ const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
17
13
  const i = Math.floor(Math.log(bytes) / Math.log(k));
18
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
14
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
19
15
  }
16
+
20
17
  function formatDate(ts: number): string {
21
- return new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
18
+ return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
22
19
  }
20
+
23
21
  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;
22
+ if (type.startsWith('image/')) return FileImage;
23
+ if (type.includes('script') || type.includes('json') || type.includes('html') || type.includes('css')) return FileCode;
24
+ if (type.includes('zip') || type.includes('tar') || type.includes('rar')) return FileArchive;
27
25
  return FileText;
28
26
  }
29
27
 
30
28
  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
- ]);
29
+ const files = useQuery(api.files.list, {}) ?? [];
30
+ const folders = useQuery(api.folders.list, {}) ?? [];
31
+ const createFolder = useMutation(api.folders.create);
32
+ const removeFile = useMutation(api.files.remove);
33
+ const removeFolder = useMutation(api.folders.remove);
34
+
46
35
  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 = () => {
36
+ const [searchQuery, setSearchQuery] = useState('');
37
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
38
+ const [newFolderName, setNewFolderName] = useState('');
39
+ const [showNewFolder, setShowNewFolder] = useState(false);
40
+
41
+ const currentFolders = useMemo(() => {
42
+ const result = folders.filter((f: any) => {
43
+ if (currentFolderId === null) return !f.parentId;
44
+ return f.parentId === currentFolderId;
45
+ });
46
+ if (searchQuery) {
47
+ const q = searchQuery.toLowerCase();
48
+ return result.filter((f: any) => f.name.toLowerCase().includes(q));
49
+ }
50
+ return result;
51
+ }, [folders, currentFolderId, searchQuery]);
52
+
53
+ const currentFiles = useMemo(() => {
54
+ const result = files.filter((f: any) => {
55
+ if (currentFolderId === null) return !f.folderId;
56
+ return f.folderId === currentFolderId;
57
+ });
58
+ if (searchQuery) {
59
+ const q = searchQuery.toLowerCase();
60
+ return result.filter((f: any) => f.name.toLowerCase().includes(q));
61
+ }
62
+ return result;
63
+ }, [files, currentFolderId, searchQuery]);
64
+
65
+ // Build breadcrumb path
66
+ const breadcrumb = useMemo(() => {
67
+ const path: { id: string | null; name: string }[] = [{ id: null, name: 'Home' }];
68
+ let id = currentFolderId;
69
+ const visited = new Set<string>();
70
+ while (id) {
71
+ if (visited.has(id)) break;
72
+ visited.add(id);
73
+ const folder = folders.find((f: any) => f._id === id);
74
+ if (folder) {
75
+ path.push({ id: folder._id, name: folder.name });
76
+ id = folder.parentId || null;
77
+ } else break;
78
+ }
79
+ return path;
80
+ }, [currentFolderId, folders]);
81
+
82
+ const handleCreateFolder = async () => {
72
83
  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("");
84
+ await createFolder({ name: newFolderName.trim(), parentId: currentFolderId as any });
85
+ setNewFolderName('');
86
+ setShowNewFolder(false);
83
87
  };
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]);
88
+
89
+ const handleDeleteFile = async (id: any) => {
90
+ if (confirm('Delete this file?')) await removeFile({ id });
92
91
  };
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 = "";
92
+
93
+ const handleDeleteFolder = async (id: any) => {
94
+ if (confirm('Delete this folder and all its contents?')) await removeFolder({ id });
101
95
  };
102
96
 
103
97
  return (
104
98
  <DashboardLayout>
105
99
  <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>
100
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
101
+ <div>
102
+ <h1 className="text-3xl font-bold">Files</h1>
103
+ <p className="text-muted-foreground">Manage files and folders stored in your workspace.</p>
104
+ </div>
108
105
  <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} />
106
+ <button onClick={() => setShowNewFolder(true)} className="bg-muted text-foreground px-3 py-2 rounded-lg text-sm hover:bg-muted/80 flex items-center gap-2">
107
+ <FolderPlus className="w-4 h-4" /> New Folder
108
+ </button>
112
109
  </div>
113
110
  </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>
111
+
112
+ {/* Breadcrumb */}
113
+ <nav className="flex items-center gap-1 text-sm">
114
+ {breadcrumb.map((item, i) => (
115
+ <div key={item.id ?? 'root'} className="flex items-center gap-1">
116
+ {i > 0 && <ChevronRight className="w-4 h-4 text-muted-foreground" />}
117
+ <button onClick={() => setCurrentFolderId(item.id)} className={`hover:text-primary ${currentFolderId === item.id ? 'text-foreground font-medium' : 'text-muted-foreground'}`}>
118
+ {i === 0 ? <Home className="w-4 h-4 inline" /> : item.name}
119
+ </button>
130
120
  </div>
121
+ ))}
122
+ </nav>
123
+
124
+ {/* Toolbar */}
125
+ <div className="flex flex-col sm:flex-row gap-3">
126
+ <div className="relative flex-1 max-w-sm">
127
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
128
+ <input type="text" placeholder="Search files..." 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" />
129
+ </div>
130
+ <div className="flex gap-1 bg-muted p-1 rounded-lg">
131
+ <button onClick={() => setViewMode('grid')} className={`p-1.5 rounded ${viewMode === 'grid' ? 'bg-card shadow-sm' : ''}`}><Grid className="w-4 h-4" /></button>
132
+ <button onClick={() => setViewMode('list')} className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-card shadow-sm' : ''}`}><List className="w-4 h-4" /></button>
131
133
  </div>
132
134
  </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>
135
+
136
+ {/* New Folder Input */}
137
+ {showNewFolder && (
138
+ <div className="flex items-center gap-2 bg-card border border-border rounded-lg p-3">
139
+ <FolderPlus className="w-5 h-5 text-primary" />
140
+ <input type="text" value={newFolderName} onChange={(e) => setNewFolderName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()} placeholder="Folder name..." className="flex-1 bg-transparent border-none outline-none text-sm" autoFocus />
141
+ <button onClick={handleCreateFolder} className="bg-primary text-primary-foreground px-3 py-1 rounded text-sm">Create</button>
142
+ <button onClick={() => setShowNewFolder(false)} className="text-muted-foreground hover:text-foreground"><X className="w-4 h-4" /></button>
143
+ </div>
144
+ )}
145
+
146
+ {/* Content */}
147
+ {currentFolders.length === 0 && currentFiles.length === 0 ? (
148
+ <div className="text-center py-16 bg-card border border-border rounded-lg">
149
+ <File className="w-16 h-16 text-muted-foreground/30 mx-auto mb-4" />
150
+ <h3 className="text-lg font-semibold mb-2">{searchQuery ? 'No results' : 'This folder is empty'}</h3>
151
+ <p className="text-muted-foreground">Upload files via the CLI with <code className="bg-muted px-1 rounded">agentforge files upload</code></p>
152
+ </div>
153
+ ) : viewMode === 'grid' ? (
154
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
155
+ {currentFolders.map((folder: any) => (
156
+ <div key={folder._id} className="bg-card border border-border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer group" onDoubleClick={() => setCurrentFolderId(folder._id)}>
157
+ <div className="flex items-center justify-between mb-2">
158
+ <Folder className="w-8 h-8 text-primary" />
159
+ <button onClick={(e) => { e.stopPropagation(); handleDeleteFolder(folder._id); }} className="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-destructive/10"><Trash2 className="w-3.5 h-3.5 text-destructive" /></button>
149
160
  </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>
161
+ <p className="text-sm font-medium truncate">{folder.name}</p>
162
+ <p className="text-xs text-muted-foreground">{formatDate(folder.createdAt)}</p>
163
+ </div>
164
+ ))}
165
+ {currentFiles.map((file: any) => {
166
+ const Icon = getFileIcon(file.mimeType);
167
+ return (
168
+ <div key={file._id} className="bg-card border border-border rounded-lg p-4 hover:shadow-md transition-shadow group">
169
+ <div className="flex items-center justify-between mb-2">
170
+ <Icon className="w-8 h-8 text-muted-foreground" />
171
+ <button onClick={() => handleDeleteFile(file._id)} className="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-destructive/10"><Trash2 className="w-3.5 h-3.5 text-destructive" /></button>
157
172
  </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>
173
+ <p className="text-sm font-medium truncate">{file.name}</p>
174
+ <p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p>
159
175
  </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>
176
+ );
177
+ })}
178
+ </div>
179
+ ) : (
180
+ <div className="bg-card border border-border rounded-lg overflow-hidden">
181
+ <table className="w-full text-sm">
182
+ <thead className="bg-muted/50">
183
+ <tr>
184
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Name</th>
185
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Type</th>
186
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Size</th>
187
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Date</th>
188
+ <th className="text-right px-4 py-3 font-medium text-muted-foreground">Actions</th>
189
+ </tr>
190
+ </thead>
191
+ <tbody>
192
+ {currentFolders.map((folder: any) => (
193
+ <tr key={folder._id} className="border-t border-border hover:bg-muted/30 cursor-pointer" onDoubleClick={() => setCurrentFolderId(folder._id)}>
194
+ <td className="px-4 py-3 flex items-center gap-2"><Folder className="w-4 h-4 text-primary" />{folder.name}</td>
195
+ <td className="px-4 py-3 text-muted-foreground">Folder</td>
196
+ <td className="px-4 py-3 text-muted-foreground">—</td>
197
+ <td className="px-4 py-3 text-muted-foreground">{formatDate(folder.createdAt)}</td>
198
+ <td className="px-4 py-3 text-right"><button onClick={() => handleDeleteFolder(folder._id)} className="p-1.5 rounded hover:bg-destructive/10"><Trash2 className="w-4 h-4 text-destructive" /></button></td>
199
+ </tr>
200
+ ))}
201
+ {currentFiles.map((file: any) => {
202
+ const Icon = getFileIcon(file.mimeType);
203
+ return (
204
+ <tr key={file._id} className="border-t border-border hover:bg-muted/30">
205
+ <td className="px-4 py-3 flex items-center gap-2"><Icon className="w-4 h-4 text-muted-foreground" />{file.name}</td>
206
+ <td className="px-4 py-3 text-muted-foreground">{file.mimeType}</td>
207
+ <td className="px-4 py-3 text-muted-foreground">{formatFileSize(file.size)}</td>
208
+ <td className="px-4 py-3 text-muted-foreground">{formatDate(file.uploadedAt)}</td>
209
+ <td className="px-4 py-3 text-right"><button onClick={() => handleDeleteFile(file._id)} className="p-1.5 rounded hover:bg-destructive/10"><Trash2 className="w-4 h-4 text-destructive" /></button></td>
179
210
  </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>
211
+ );
212
+ })}
213
+ </tbody>
214
+ </table>
215
+ </div>
199
216
  )}
200
217
  </div>
201
218
  </DashboardLayout>