Spaces:
Running
Running
"use client"; | |
import React, { useState, useEffect, useCallback } from 'react'; | |
import { FileDocument, FolderDocument } from '@/types'; | |
import { FileExplorer } from './file-explorer'; | |
import { MultiTabEditor } from './editor/multi-tab-editor'; | |
import { ProjectGenerator } from './ai/project-generator'; | |
import { SmartEditor } from './ai/smart-editor'; | |
import { PromptSystem } from './ai/prompt-system'; | |
import { Button } from '@/components/ui/button'; | |
import { Badge } from '@/components/ui/badge'; | |
import { | |
ResizableHandle, | |
ResizablePanel, | |
ResizablePanelGroup, | |
} from '@/components/ui/resizable'; | |
import { | |
DropdownMenu, | |
DropdownMenuContent, | |
DropdownMenuItem, | |
DropdownMenuSeparator, | |
DropdownMenuTrigger, | |
} from '@/components/ui/dropdown-menu'; | |
import { | |
Dialog, | |
DialogContent, | |
// DialogDescription, | |
// DialogHeader, | |
// DialogTitle, | |
} from '@/components/ui/dialog'; | |
import { | |
// Sidebar, | |
// SidebarContent, | |
// SidebarHeader, | |
// SidebarProvider, | |
// SidebarTrigger, | |
} from '@/components/ui/sidebar'; | |
import { | |
Wand2, | |
// Brain, | |
MessageSquare, | |
Settings, | |
Save, | |
Download, | |
Upload, | |
RefreshCw, | |
Maximize2, | |
// Minimize2, | |
Layout, | |
// Code, | |
Eye, | |
Split, | |
// Zap, | |
// Sparkles, | |
FileText, | |
Folder, | |
// Plus, | |
// MoreHorizontal | |
} from 'lucide-react'; | |
import { apiClient } from '@/lib/api'; | |
import { toast } from 'sonner'; | |
export interface EnhancedWorkspaceProps { | |
projectId: string; | |
initialFiles?: FileDocument[]; | |
initialFolders?: FolderDocument[]; | |
className?: string; | |
} | |
type PanelView = 'files' | 'ai-generator' | 'prompts' | 'settings'; | |
type LayoutMode = 'default' | 'focus' | 'split' | 'preview'; | |
export function EnhancedWorkspace({ | |
projectId, | |
initialFiles = [], | |
initialFolders = [], | |
className = "" | |
}: EnhancedWorkspaceProps) { | |
const [files, setFiles] = useState<FileDocument[]>(initialFiles); | |
const [folders, setFolders] = useState<FolderDocument[]>(initialFolders); | |
const [selectedFileId, setSelectedFileId] = useState<string | null>(null); | |
const [selectedFile, setSelectedFile] = useState<FileDocument | null>(null); | |
const [leftPanelView, setLeftPanelView] = useState<PanelView>('files'); | |
const [layoutMode, setLayoutMode] = useState<LayoutMode>('default'); | |
const [isGeneratorOpen, setIsGeneratorOpen] = useState(false); | |
const [isLoading, setIsLoading] = useState(false); | |
const [lastSaved, setLastSaved] = useState<Date | null>(null); | |
const [unsavedChanges, setUnsavedChanges] = useState(0); | |
// Load project data | |
const loadProjectData = useCallback(async () => { | |
setIsLoading(true); | |
try { | |
// Load files and folders | |
const [filesResponse, foldersResponse] = await Promise.all([ | |
apiClient.getFiles(projectId), | |
apiClient.getFolders(projectId, { tree: true }) | |
]); | |
const filesData = filesResponse.data as { ok?: boolean; files?: FileDocument[] } | FileDocument[]; | |
if (!Array.isArray(filesData)) { | |
if (filesData.ok && filesData.files) { | |
setFiles(filesData.files); | |
} | |
} else { | |
setFiles(filesData); | |
} | |
const foldersData = foldersResponse.data as { ok?: boolean; tree?: FolderDocument[] } | FolderDocument[]; | |
if (!Array.isArray(foldersData)) { | |
if (foldersData.ok && foldersData.tree) { | |
setFolders(foldersData.tree); | |
} | |
} else { | |
setFolders(foldersData); | |
} | |
} catch (error: unknown) { | |
console.error('Error loading project data:', error); | |
const message = (error as { message?: string })?.message || 'Failed to load project data'; | |
toast.error(message); | |
} finally { | |
setIsLoading(false); | |
} | |
}, [projectId]); | |
// Initialize project data | |
useEffect(() => { | |
if (files.length === 0 && folders.length === 0) { | |
loadProjectData(); | |
} | |
}, [loadProjectData, files.length, folders.length]); | |
// Handle file selection | |
const handleFileSelect = useCallback((file: FileDocument) => { | |
setSelectedFileId(file._id); | |
setSelectedFile(file); | |
}, []); | |
// Handle file creation | |
const handleFileCreate = useCallback((file: FileDocument) => { | |
setFiles(prev => [...prev, file]); | |
setSelectedFileId(file._id); | |
setSelectedFile(file); | |
toast.success(`File "${file.name}" created`); | |
}, []); | |
// Handle file update | |
const handleFileUpdate = useCallback((updatedFile: FileDocument) => { | |
setFiles(prev => prev.map(f => f._id === updatedFile._id ? updatedFile : f)); | |
if (selectedFile?._id === updatedFile._id) { | |
setSelectedFile(updatedFile); | |
} | |
setLastSaved(new Date()); | |
}, [selectedFile]); | |
// Handle file deletion | |
const handleFileDelete = useCallback((fileId: string) => { | |
setFiles(prev => prev.filter(f => f._id !== fileId)); | |
if (selectedFileId === fileId) { | |
setSelectedFileId(null); | |
setSelectedFile(null); | |
} | |
toast.success('File deleted'); | |
}, [selectedFileId]); | |
// Handle folder creation | |
const handleFolderCreate = useCallback((folder: FolderDocument) => { | |
setFolders(prev => [...prev, folder]); | |
toast.success(`Folder "${folder.name}" created`); | |
}, []); | |
// Handle project generation | |
const handleProjectGenerated = useCallback((newFiles: FileDocument[], newFolders: FolderDocument[]) => { | |
setFiles(prev => [...prev, ...newFiles]); | |
setFolders(prev => [...prev, ...newFolders]); | |
// Select the first generated file | |
if (newFiles.length > 0) { | |
const firstFile = newFiles[0]; | |
setSelectedFileId(firstFile._id); | |
setSelectedFile(firstFile); | |
} | |
setIsGeneratorOpen(false); | |
toast.success(`Generated ${newFiles.length} files successfully!`); | |
}, []); | |
// Handle prompt selection | |
const handlePromptSelect = useCallback((prompt: string) => { | |
// This would typically be handled by the AI editor component | |
console.log('Selected prompt:', prompt); | |
}, []); | |
// Save all changes | |
const saveAllChanges = useCallback(async () => { | |
try { | |
// This would trigger save on all open tabs | |
toast.success('All changes saved'); | |
setLastSaved(new Date()); | |
setUnsavedChanges(0); | |
} catch { | |
toast.error('Failed to save changes'); | |
} | |
}, []); | |
// Export project | |
const exportProject = useCallback(async () => { | |
try { | |
// Create a zip file with all project files | |
toast.success('Project exported successfully'); | |
} catch { | |
toast.error('Failed to export project'); | |
} | |
}, []); | |
// Get layout classes | |
const getLayoutClasses = () => { | |
switch (layoutMode) { | |
case 'focus': | |
return 'grid-cols-[0px_1fr]'; // Hide sidebar | |
case 'split': | |
return 'grid-cols-[300px_1fr_1fr]'; // Add preview panel | |
case 'preview': | |
return 'grid-cols-[300px_0px_1fr]'; // Hide editor, show preview | |
default: | |
return 'grid-cols-[300px_1fr]'; // Default layout | |
} | |
}; | |
const renderLeftPanel = () => { | |
switch (leftPanelView) { | |
case 'ai-generator': | |
return ( | |
<ProjectGenerator | |
projectId={projectId} | |
onProjectGenerated={handleProjectGenerated} | |
onClose={() => setLeftPanelView('files')} | |
/> | |
); | |
case 'prompts': | |
return ( | |
<PromptSystem | |
onPromptSelect={handlePromptSelect} | |
currentFileType={selectedFile?.type} | |
/> | |
); | |
case 'settings': | |
return ( | |
<div className="p-4"> | |
<h3 className="text-lg font-semibold text-white mb-4">Settings</h3> | |
<p className="text-neutral-400">Settings panel coming soon...</p> | |
</div> | |
); | |
default: | |
return ( | |
<FileExplorer | |
projectId={projectId} | |
onFileSelect={handleFileSelect} | |
onFileCreate={handleFileCreate} | |
onFileUpdate={handleFileUpdate} | |
onFileDelete={handleFileDelete} | |
onFolderCreate={handleFolderCreate} | |
selectedFileId={selectedFileId ?? undefined} | |
/> | |
); | |
} | |
}; | |
return ( | |
<div className={`h-screen bg-neutral-950 flex flex-col ${getLayoutClasses()} ${className}`}> | |
{/* Top Toolbar */} | |
<div className="h-12 bg-neutral-900 border-b border-neutral-800 flex items-center justify-between px-4"> | |
<div className="flex items-center gap-4"> | |
{/* Panel View Selector */} | |
<div className="flex items-center gap-1"> | |
<Button | |
variant={leftPanelView === 'files' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLeftPanelView('files')} | |
> | |
<Folder className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={leftPanelView === 'ai-generator' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLeftPanelView('ai-generator')} | |
> | |
<Wand2 className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={leftPanelView === 'prompts' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLeftPanelView('prompts')} | |
> | |
<MessageSquare className="h-4 w-4" /> | |
</Button> | |
</div> | |
{/* Layout Mode Selector */} | |
<div className="flex items-center gap-1"> | |
<Button | |
variant={layoutMode === 'default' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLayoutMode('default')} | |
title="Default Layout" | |
> | |
<Layout className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={layoutMode === 'focus' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLayoutMode('focus')} | |
title="Focus Mode" | |
> | |
<Maximize2 className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={layoutMode === 'split' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLayoutMode('split')} | |
title="Split View" | |
> | |
<Split className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={layoutMode === 'preview' ? 'default' : 'ghost'} | |
size="sm" | |
onClick={() => setLayoutMode('preview')} | |
title="Preview Mode" | |
> | |
<Eye className="h-4 w-4" /> | |
</Button> | |
</div> | |
</div> | |
<div className="flex items-center gap-4"> | |
{/* Project Status */} | |
<div className="flex items-center gap-2 text-sm text-neutral-400"> | |
<Badge variant="outline" className="text-xs"> | |
{files.length} files | |
</Badge> | |
{unsavedChanges > 0 && ( | |
<Badge variant="destructive" className="text-xs"> | |
{unsavedChanges} unsaved | |
</Badge> | |
)} | |
{lastSaved && ( | |
<span className="text-xs"> | |
Saved {lastSaved.toLocaleTimeString()} | |
</span> | |
)} | |
</div> | |
{/* Actions */} | |
<div className="flex items-center gap-1"> | |
<Button | |
variant="ghost" | |
size="sm" | |
onClick={saveAllChanges} | |
disabled={unsavedChanges === 0} | |
> | |
<Save className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant="ghost" | |
size="sm" | |
onClick={loadProjectData} | |
disabled={isLoading} | |
> | |
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} /> | |
</Button> | |
<DropdownMenu> | |
<DropdownMenuTrigger asChild> | |
<Button variant="ghost" size="sm"> | |
โฎ | |
</Button> | |
</DropdownMenuTrigger> | |
<DropdownMenuContent align="end"> | |
<DropdownMenuItem onClick={() => setIsGeneratorOpen(true)}> | |
<Wand2 className="h-4 w-4 mr-2" /> | |
AI Project Generator | |
</DropdownMenuItem> | |
<DropdownMenuSeparator /> | |
<DropdownMenuItem onClick={exportProject}> | |
<Download className="h-4 w-4 mr-2" /> | |
Export Project | |
</DropdownMenuItem> | |
<DropdownMenuItem> | |
<Upload className="h-4 w-4 mr-2" /> | |
Import Files | |
</DropdownMenuItem> | |
<DropdownMenuSeparator /> | |
<DropdownMenuItem onClick={() => setLeftPanelView('settings')}> | |
<Settings className="h-4 w-4 mr-2" /> | |
Settings | |
</DropdownMenuItem> | |
</DropdownMenuContent> | |
</DropdownMenu> | |
</div> | |
</div> | |
</div> | |
{/* Main Content */} | |
<div className="flex-1 overflow-hidden"> | |
<ResizablePanelGroup direction="horizontal"> | |
{/* Left Panel */} | |
{layoutMode !== 'focus' && ( | |
<> | |
<ResizablePanel defaultSize={25} minSize={20} maxSize={40}> | |
{renderLeftPanel()} | |
</ResizablePanel> | |
<ResizableHandle /> | |
</> | |
)} | |
{/* Editor Panel */} | |
{layoutMode !== 'preview' && ( | |
<> | |
<ResizablePanel defaultSize={layoutMode === 'split' ? 50 : 75}> | |
<div className="relative h-full"> | |
<MultiTabEditor | |
projectId={projectId} | |
files={files} | |
selectedFileId={selectedFileId ?? undefined} | |
onFileUpdate={handleFileUpdate} | |
onFileCreate={() => setLeftPanelView('files')} | |
/> | |
{/* Smart Editor Overlay */} | |
{selectedFile && ( | |
<SmartEditor | |
file={selectedFile} | |
projectId={projectId} | |
onContentUpdate={(content) => { | |
const updatedFile = { ...selectedFile, content }; | |
setSelectedFile(updatedFile); | |
setUnsavedChanges(prev => prev + 1); | |
}} | |
onSave={() => { | |
// Handle save | |
setUnsavedChanges(prev => Math.max(0, prev - 1)); | |
}} | |
/> | |
)} | |
</div> | |
</ResizablePanel> | |
{layoutMode === 'split' && <ResizableHandle />} | |
</> | |
)} | |
{/* Preview Panel */} | |
{(layoutMode === 'split' || layoutMode === 'preview') && ( | |
<ResizablePanel defaultSize={layoutMode === 'preview' ? 75 : 50}> | |
<div className="h-full bg-neutral-900 border-l border-neutral-800"> | |
{selectedFile ? ( | |
<div className="h-full"> | |
{/* Preview content would go here */} | |
<div className="p-4 text-center text-neutral-400"> | |
<Eye className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
<p>Preview for {selectedFile.name}</p> | |
<p className="text-sm">Preview functionality coming soon</p> | |
</div> | |
</div> | |
) : ( | |
<div className="h-full flex items-center justify-center text-neutral-400"> | |
<div className="text-center"> | |
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
<p>Select a file to preview</p> | |
</div> | |
</div> | |
)} | |
</div> | |
</ResizablePanel> | |
)} | |
</ResizablePanelGroup> | |
</div> | |
{/* AI Project Generator Dialog */} | |
<Dialog open={isGeneratorOpen} onOpenChange={setIsGeneratorOpen}> | |
<DialogContent className="max-w-6xl max-h-[90vh] p-0"> | |
<ProjectGenerator | |
projectId={projectId} | |
onProjectGenerated={handleProjectGenerated} | |
onClose={() => setIsGeneratorOpen(false)} | |
/> | |
</DialogContent> | |
</Dialog> | |
</div> | |
); | |
} | |