aethex-studio/src/App.tsx

361 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ...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;