@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.
- package/dist/default/convex/agents.ts +204 -0
- package/dist/default/convex/apiKeys.ts +133 -0
- package/dist/default/convex/cronJobs.ts +224 -0
- package/dist/default/convex/files.ts +103 -0
- package/dist/default/convex/folders.ts +110 -0
- package/dist/default/convex/heartbeat.ts +371 -0
- package/dist/default/convex/logs.ts +66 -0
- package/dist/default/convex/mastraIntegration.ts +185 -0
- package/dist/default/convex/mcpConnections.ts +127 -0
- package/dist/default/convex/messages.ts +90 -0
- package/dist/default/convex/projects.ts +114 -0
- package/dist/default/convex/schema.ts +150 -83
- package/dist/default/convex/sessions.ts +174 -0
- package/dist/default/convex/settings.ts +79 -0
- package/dist/default/convex/skills.ts +178 -0
- package/dist/default/convex/threads.ts +100 -0
- package/dist/default/convex/usage.ts +195 -0
- package/dist/default/convex/vault.ts +397 -0
- package/dist/default/dashboard/app/main.tsx +7 -3
- package/dist/default/dashboard/app/routes/agents.tsx +103 -161
- package/dist/default/dashboard/app/routes/chat.tsx +163 -317
- package/dist/default/dashboard/app/routes/connections.tsx +247 -386
- package/dist/default/dashboard/app/routes/cron.tsx +127 -286
- package/dist/default/dashboard/app/routes/files.tsx +184 -167
- package/dist/default/dashboard/app/routes/index.tsx +63 -96
- package/dist/default/dashboard/app/routes/projects.tsx +106 -225
- package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
- package/dist/default/dashboard/app/routes/settings.tsx +316 -532
- package/dist/default/dashboard/app/routes/skills.tsx +329 -216
- package/dist/default/dashboard/app/routes/usage.tsx +107 -150
- package/dist/default/dashboard/tsconfig.json +3 -2
- package/dist/default/dashboard/vite.config.ts +6 -0
- package/dist/index.js +256 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/convex/agents.ts +204 -0
- package/templates/default/convex/apiKeys.ts +133 -0
- package/templates/default/convex/cronJobs.ts +224 -0
- package/templates/default/convex/files.ts +103 -0
- package/templates/default/convex/folders.ts +110 -0
- package/templates/default/convex/heartbeat.ts +371 -0
- package/templates/default/convex/logs.ts +66 -0
- package/templates/default/convex/mastraIntegration.ts +185 -0
- package/templates/default/convex/mcpConnections.ts +127 -0
- package/templates/default/convex/messages.ts +90 -0
- package/templates/default/convex/projects.ts +114 -0
- package/templates/default/convex/schema.ts +150 -83
- package/templates/default/convex/sessions.ts +174 -0
- package/templates/default/convex/settings.ts +79 -0
- package/templates/default/convex/skills.ts +178 -0
- package/templates/default/convex/threads.ts +100 -0
- package/templates/default/convex/usage.ts +195 -0
- package/templates/default/convex/vault.ts +397 -0
- package/templates/default/dashboard/app/main.tsx +7 -3
- package/templates/default/dashboard/app/routes/agents.tsx +103 -161
- package/templates/default/dashboard/app/routes/chat.tsx +163 -317
- package/templates/default/dashboard/app/routes/connections.tsx +247 -386
- package/templates/default/dashboard/app/routes/cron.tsx +127 -286
- package/templates/default/dashboard/app/routes/files.tsx +184 -167
- package/templates/default/dashboard/app/routes/index.tsx +63 -96
- package/templates/default/dashboard/app/routes/projects.tsx +106 -225
- package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
- package/templates/default/dashboard/app/routes/settings.tsx +316 -532
- package/templates/default/dashboard/app/routes/skills.tsx +329 -216
- package/templates/default/dashboard/app/routes/usage.tsx +107 -150
- package/templates/default/dashboard/tsconfig.json +3 -2
- package/templates/default/dashboard/vite.config.ts +6 -0
|
@@ -1,201 +1,218 @@
|
|
|
1
|
-
import { createFileRoute } from
|
|
2
|
-
import { DashboardLayout } from
|
|
3
|
-
import { useState,
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
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(
|
|
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
|
|
16
|
-
const k = 1024, sizes = [
|
|
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)) +
|
|
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(
|
|
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(
|
|
25
|
-
if (type.includes(
|
|
26
|
-
if (type.includes(
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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<
|
|
49
|
-
const [
|
|
50
|
-
const [
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
setNewFolderName(
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
107
|
-
<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={() =>
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
</
|
|
183
|
-
</
|
|
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>
|