omnidev / components /enhanced-workspace.tsx
kalhdrawi's picture
ุฃูˆู„ ุฑูุน ู„ู„ู…ู„ูุงุช ุฅู„ู‰ ุงู„ุณุจูŠุณ kalhdrawi/omnidev
1cf8f01
raw
history blame
16.3 kB
"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>
);
}