361 lines
17 KiB
TypeScript
361 lines
17 KiB
TypeScript
// ...existing code...
|
||
import React, { useState, lazy, Suspense } from 'react';
|
||
import { FileTree } from '../components/FileTree';
|
||
import { FileTabs } from '../components/FileTabs';
|
||
import { CodeEditor } from '../components/CodeEditor';
|
||
import { ConsolePanel } from '../components/ConsolePanel';
|
||
import { Toolbar } from './components/Toolbar';
|
||
import { AIAssistant } from '../components/AIAssistant';
|
||
import { CrossPlatformPreview } from '../components/CrossPlatformPreview';
|
||
import { NexusSyncMonitor } from '../components/NexusSyncMonitor';
|
||
import { Toaster } from './components/ui/sonner';
|
||
import { toast } from 'sonner';
|
||
import { useEditorStore } from '../store/editor-store';
|
||
import { FileNode } from '../store/editor-store';
|
||
import { captureEvent } from './lib/posthog';
|
||
import { captureError } from './lib/sentry';
|
||
import { useIsMobile } from './hooks/use-mobile';
|
||
|
||
const TemplatesDrawer = lazy(() => import('./components/TemplatesDrawer'));
|
||
const PreviewModal = lazy(() => import('./components/PreviewModal'));
|
||
const NewProjectModal = lazy(() => import('./components/NewProjectModal'));
|
||
const PassportLogin = lazy(() => import('./components/PassportLogin'));
|
||
const TranslationPanel = lazy(() => import('./components/TranslationPanel'));
|
||
const CommandPalette = lazy(() => import('./components/CommandPalette'));
|
||
const StudioSidebar = lazy(() => import('../components/StudioSidebar'));
|
||
const StudioEditor = lazy(() => import('../components/StudioEditor'));
|
||
// const StudioBottomPanel = lazy(() => import('../components/StudioBottomPanel'));
|
||
const StudioRightPanel = lazy(() => import('../components/StudioRightPanel'));
|
||
const StudioNetworkViz = lazy(() => import('../components/StudioNetworkViz'));
|
||
|
||
function App() {
|
||
// --- Error/Warning Banner State ---
|
||
const [problemsExpanded, setProblemsExpanded] = useState(false);
|
||
// --- Right Sidebar Tab State ---
|
||
const [rightSidebarTab, setRightSidebarTab] = useState('Copilot');
|
||
// TODO: Connect to real error/warning data source
|
||
const problems: Array<{ file: string; line: number; type: string; message: string }> = [];
|
||
// ...existing state and hooks...
|
||
// --- State ---
|
||
const setFiles = useEditorStore((state) => state.setFiles);
|
||
const [currentCode, setCurrentCode] = useState('');
|
||
const [showTemplates, setShowTemplates] = useState(false);
|
||
const [showPreview, setShowPreview] = useState(false);
|
||
const [showNewProject, setShowNewProject] = useState(false);
|
||
const [showFileSearch, setShowFileSearch] = useState(false);
|
||
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
||
const [showSearchInFiles, setShowSearchInFiles] = useState(false);
|
||
const [showTranslation, setShowTranslation] = useState(false);
|
||
const [code, setCode] = useState('');
|
||
const [currentPlatform, setCurrentPlatform] = useState('roblox');
|
||
const isMobile = useIsMobile();
|
||
const [showPassportLogin, setShowPassportLogin] = useState(false);
|
||
const [consoleCollapsed, setConsoleCollapsed] = useState(isMobile);
|
||
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(() => {
|
||
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
|
||
return stored ? JSON.parse(stored) : null;
|
||
});
|
||
const [openFiles, setOpenFiles] = useState<FileNode[]>([]);
|
||
const [activeFileId, setActiveFileId] = useState<string>('');
|
||
|
||
// --- Handlers ---
|
||
const handleTemplateSelect = (templateCode: string) => {
|
||
setCode(templateCode);
|
||
setCurrentCode(templateCode);
|
||
if (activeFileId) {
|
||
handleCodeChange(templateCode);
|
||
}
|
||
};
|
||
|
||
const handleCodeChange = (newCode: string) => {
|
||
setCurrentCode(newCode);
|
||
setCode(newCode);
|
||
if (activeFileId) {
|
||
const files = useEditorStore.getState().files;
|
||
const updateFileContent = (nodes: FileNode[]): FileNode[] => {
|
||
return nodes.map((node) => {
|
||
if (node.id === activeFileId) {
|
||
return { ...node, content: newCode };
|
||
}
|
||
if (node.children) {
|
||
return { ...node, children: updateFileContent(node.children) };
|
||
}
|
||
return node;
|
||
});
|
||
};
|
||
setFiles(updateFileContent(files || []));
|
||
setOpenFiles((prev) => (prev || []).map((file) => file.id === activeFileId ? { ...file, content: newCode } : file));
|
||
}
|
||
};
|
||
|
||
const handleFileSelect = (file: FileNode) => {
|
||
if (file.type === 'file') {
|
||
setActiveFileId(file.id);
|
||
if (!(openFiles || []).find((f) => f.id === file.id)) {
|
||
setOpenFiles((prev) => [...(prev || []), file]);
|
||
}
|
||
setCode(file.content || '');
|
||
setCurrentCode(file.content || '');
|
||
}
|
||
captureEvent('file_select', { fileId: file.id });
|
||
};
|
||
|
||
const handleFileClose = (id: string) => {
|
||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||
};
|
||
|
||
const handleFileRename = (id: string, newName: string) => {
|
||
if (!newName || newName.trim() === '') {
|
||
toast.error('File name cannot be empty');
|
||
return;
|
||
}
|
||
const files = useEditorStore.getState().files;
|
||
const rename = (nodes: FileNode[]): FileNode[] => {
|
||
return nodes.map((node) => {
|
||
if (node.id === id) {
|
||
return { ...node, name: newName };
|
||
}
|
||
if (node.children) {
|
||
return { ...node, children: rename(node.children) };
|
||
}
|
||
return node;
|
||
});
|
||
};
|
||
setFiles(rename(files || []));
|
||
captureEvent('file_rename', { id, newName });
|
||
};
|
||
|
||
const handleFileDelete = (id: string) => {
|
||
try {
|
||
const files = useEditorStore.getState().files;
|
||
const deleteNode = (nodes: FileNode[]): FileNode[] => {
|
||
return nodes.filter((node) => {
|
||
if (node.id === id) return false;
|
||
if (node.children) {
|
||
node.children = deleteNode(node.children);
|
||
}
|
||
return true;
|
||
});
|
||
};
|
||
setFiles(deleteNode(files || []));
|
||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||
if (activeFileId === id) {
|
||
setActiveFileId((openFiles || [])[0]?.id || '');
|
||
}
|
||
captureEvent('file_delete', { id });
|
||
} catch (error) {
|
||
console.error('Failed to delete file:', error);
|
||
captureError(error as Error, { context: 'file_delete', id });
|
||
toast.error('Failed to delete file. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleFileMove = (fileId: string, targetParentId: string) => {
|
||
try {
|
||
const files = useEditorStore.getState().files;
|
||
let movedNode: FileNode | null = null;
|
||
const removeNode = (nodes: FileNode[]): FileNode[] => {
|
||
return nodes.filter((node) => {
|
||
if (node.id === fileId) {
|
||
movedNode = node;
|
||
return false;
|
||
}
|
||
if (node.children) {
|
||
node.children = removeNode(node.children);
|
||
}
|
||
return true;
|
||
});
|
||
};
|
||
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
||
return nodes.map((node) => {
|
||
if (node.id === targetParentId && node.type === 'folder') {
|
||
return {
|
||
...node,
|
||
children: [...(node.children || []), movedNode!],
|
||
};
|
||
}
|
||
if (node.children) {
|
||
return { ...node, children: addToTarget(node.children) };
|
||
}
|
||
return node;
|
||
});
|
||
};
|
||
const withoutMoved = removeNode(files || []);
|
||
if (movedNode) {
|
||
setFiles(addToTarget(withoutMoved));
|
||
} else {
|
||
setFiles(files || []);
|
||
}
|
||
captureEvent('file_move', { fileId, targetParentId });
|
||
} catch (error) {
|
||
console.error('Failed to move file:', error);
|
||
captureError(error as Error, { context: 'file_move', fileId, targetParentId });
|
||
toast.error('Failed to move file. Please try again.');
|
||
}
|
||
};
|
||
|
||
// --- Render ---
|
||
return (
|
||
<>
|
||
<Toaster position="bottom-right" />
|
||
<div className="h-screen w-screen bg-background flex flex-col">
|
||
<div className="flex flex-1 overflow-hidden">
|
||
{/* Left Sidebar: Vertical tabs + Explorer */}
|
||
<div className="w-16 h-full bg-[#20232A] border-r border-border flex flex-col items-center py-2 gap-2">
|
||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Explorer">
|
||
<span className="text-xl">📁</span>
|
||
</button>
|
||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Assets">
|
||
<span className="text-xl">🗂️</span>
|
||
</button>
|
||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Search">
|
||
<span className="text-xl">🔍</span>
|
||
</button>
|
||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Source Control">
|
||
<span className="text-xl">🔗</span>
|
||
</button>
|
||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Extensions">
|
||
<span className="text-xl">🧩</span>
|
||
</button>
|
||
</div>
|
||
<div className="w-56 h-full bg-card border-r border-border flex flex-col shadow-lg p-2">
|
||
<Suspense fallback={<div className="p-4">Loading...</div>}>
|
||
<FileTree />
|
||
</Suspense>
|
||
</div>
|
||
{/* Center: Tabs + Editor + Preview */}
|
||
<div className="flex-1 flex flex-col">
|
||
{/* File Tabs Row (no workspace tabs above) */}
|
||
<div className="h-10 flex items-center bg-[#23272F] border-b border-border px-2">
|
||
<FileTabs />
|
||
</div>
|
||
{/* Main Editor/Preview Split */}
|
||
<div className="flex flex-1 overflow-hidden">
|
||
<div className="flex-1 flex flex-col">
|
||
<Toolbar code={code} onTemplatesClick={() => setShowTemplates(true)} /*...existing code...*/ />
|
||
<div className="flex-1 flex flex-col">
|
||
{activeFileId ? (
|
||
<CodeEditor />
|
||
) : (
|
||
<div className="flex flex-1 flex-col items-center justify-center text-center gap-6 bg-background/80">
|
||
<div className="text-6xl">📂</div>
|
||
<div>
|
||
<h2 className="text-xl font-bold mb-2">Welcome to AeThex Studio</h2>
|
||
<p className="text-gray-400 mb-4">Select a file from the explorer to start editing, or use the quick actions below.</p>
|
||
<div className="flex flex-wrap gap-2 justify-center">
|
||
<button className="px-4 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700" onClick={() => setShowNewProject(true)}>New Project</button>
|
||
<button className="px-4 py-2 rounded bg-green-600 text-white text-sm hover:bg-green-700" onClick={() => setShowTemplates(true)}>Templates</button>
|
||
<button className="px-4 py-2 rounded bg-gray-700 text-white text-sm hover:bg-gray-800" onClick={() => setShowFileSearch(true)}>Open File</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col border-l border-border bg-card" style={{ minWidth: 320, maxWidth: 600, width: '40%', resize: 'horizontal', overflow: 'auto' }}>
|
||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||
<h4 className="font-bold text-base mb-2">Preview</h4>
|
||
<button className="text-xs text-gray-400 hover:text-white" onClick={() => setShowPreview((v) => !v)}>{showPreview ? 'Hide' : 'Show'}</button>
|
||
</div>
|
||
{showPreview && (
|
||
<div className="flex-1 p-4">
|
||
<CrossPlatformPreview />
|
||
</div>
|
||
)}
|
||
<div className="flex items-center justify-between p-4 border-t border-border">
|
||
<h4 className="font-bold text-base mb-2">Sync Monitor</h4>
|
||
<button className="text-xs text-gray-400 hover:text-white" onClick={() => setShowTranslation((v) => !v)}>{showTranslation ? 'Hide' : 'Show'}</button>
|
||
</div>
|
||
{showTranslation && (
|
||
<div className="p-4">
|
||
<NexusSyncMonitor />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{/* ConsolePanel moved to top bar. */}
|
||
</div>
|
||
{/* Right Sidebar: Copilot, AI, Inspector, Trinity */}
|
||
<div className="w-72 h-full border-l border-border bg-card flex flex-col shadow-lg p-2 gap-4">
|
||
<Suspense fallback={<div className="p-4">Loading...</div>}>
|
||
<div className="rounded-xl shadow-lg bg-background p-0 mb-2 h-full flex flex-col">
|
||
<div className="flex border-b border-border">
|
||
{['Copilot', 'AI', 'Inspector', 'Trinity'].map(tab => (
|
||
<button
|
||
key={tab}
|
||
className={`flex-1 py-2 text-xs font-bold uppercase tracking-wider border-b-2 transition-colors ${tab === rightSidebarTab ? 'border-blue-500 text-blue-400 bg-background' : 'border-transparent text-gray-400 bg-card hover:bg-muted/30'}`}
|
||
onClick={() => setRightSidebarTab(tab)}
|
||
>
|
||
{tab}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
{rightSidebarTab === 'Copilot' && <StudioRightPanel />}
|
||
{rightSidebarTab === 'AI' && <AIAssistant />}
|
||
{rightSidebarTab === 'Inspector' && <div>Inspector/Properties coming soon…</div>}
|
||
{rightSidebarTab === 'Trinity' && <StudioNetworkViz />}
|
||
</div>
|
||
</div>
|
||
</Suspense>
|
||
</div>
|
||
</div>
|
||
{/* Problems Details Floating Panel */}
|
||
{problemsExpanded && (
|
||
<div className="fixed top-16 left-1/2 transform -translate-x-1/2 z-50 bg-card border border-border rounded-xl shadow-xl px-8 py-6 w-[480px]">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="font-bold text-destructive">Problems</span>
|
||
<button
|
||
className="ml-4 px-3 py-1 rounded bg-muted text-xs hover:bg-muted/80 transition"
|
||
onClick={() => setProblemsExpanded(false)}
|
||
>
|
||
Hide
|
||
</button>
|
||
</div>
|
||
<ul className="space-y-2">
|
||
{problems.map((p, i) => (
|
||
<li key={i} className="flex items-center gap-3 text-sm">
|
||
<span className={p.type === 'error' ? 'text-destructive' : 'text-yellow-500'}>
|
||
{p.type === 'error' ? 'Error:' : 'Warning:'}
|
||
</span>
|
||
<span className="font-mono text-xs text-muted-foreground">{p.file}:{p.line}</span>
|
||
<span>{p.message}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
{/* Modals and Drawers */}
|
||
<Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading…</div>}>
|
||
{showTemplates && <TemplatesDrawer open={showTemplates} onSelect={handleTemplateSelect} onClose={() => setShowTemplates(false)} />}
|
||
{showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />}
|
||
{showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} />}
|
||
</Suspense>
|
||
</div>
|
||
|
||
<Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading…</div>}>
|
||
{showTemplates && <TemplatesDrawer open={showTemplates} onSelect={handleTemplateSelect} onClose={() => setShowTemplates(false)} />}
|
||
{showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />}
|
||
{showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} onCreateProject={() => {}} />}
|
||
{showTranslation && <TranslationPanel isOpen={showTranslation} onClose={() => setShowTranslation(false)} currentCode={currentCode} currentPlatform={currentPlatform} />}
|
||
{showPassportLogin && <PassportLogin open={showPassportLogin} onClose={() => setShowPassportLogin(false)} onLoginSuccess={() => {}} />}
|
||
</Suspense>
|
||
|
||
<CommandPalette
|
||
open={showCommandPalette}
|
||
onClose={() => setShowCommandPalette(false)}
|
||
commands={[
|
||
{ id: 'new-project', label: 'New Project', description: 'Create a new project', icon: '📁', action: () => setShowNewProject(true) },
|
||
{ id: 'templates', label: 'Templates', description: 'Open templates drawer', icon: '📄', action: () => setShowTemplates(true) },
|
||
{ id: 'preview', label: 'Preview', description: 'Preview your code', icon: '👁️', action: () => setShowPreview(true) },
|
||
{ id: 'export', label: 'Export', description: 'Export your project', icon: '⬇️', action: () => toast.success('Exported!') },
|
||
{ id: 'copy', label: 'Copy', description: 'Copy code', icon: '📋', action: () => toast.success('Copied!') },
|
||
]}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default App;
|
||
|
||
|