aethex-studio/src/App.tsx

361 lines
No EOL
12 KiB
TypeScript

import React, { useState } from 'react';
import { Toaster } from './components/ui/sonner';
import { CodeEditor } from './components/CodeEditor';
import { AIChat } from './components/AIChat';
import { Toolbar } from './components/Toolbar';
import { TemplatesDrawer } from './components/TemplatesDrawer';
import { WelcomeDialog } from './components/WelcomeDialog';
import { FileTree, FileNode } from './components/FileTree';
import { FileTabs } from './components/FileTabs';
import { PreviewModal } from './components/PreviewModal';
import { NewProjectModal, ProjectConfig } from './components/NewProjectModal';
import { ConsolePanel } from './components/ConsolePanel';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable';
import { useIsMobile } from './hooks/use-mobile';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
import { toast } from 'sonner';
import { EducationPanel } from './components/EducationPanel';
import { ExtraTabs } from './components/ui/tabs-extra';
import { PassportLogin } from './components/PassportLogin';
import { Button } from './components/ui/button';
import { initPostHog, captureEvent } from './lib/posthog';
import { initSentry, captureError } from './lib/sentry';
function App() {
const [currentCode, setCurrentCode] = useState('');
const [showTemplates, setShowTemplates] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [showNewProject, setShowNewProject] = useState(false);
const [code, setCode] = useState('');
const isMobile = useIsMobile();
const [showPassportLogin, setShowPassportLogin] = useState(false);
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;
});
React.useEffect(() => {
initPostHog();
initSentry();
}, []);
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
setUser(user);
localStorage.setItem('aethex-user', JSON.stringify(user));
captureEvent('login', { user });
};
const handleSignOut = () => {
setUser(null);
localStorage.removeItem('aethex-user');
};
const [files, setFiles] = useState<FileNode[]>([
{
id: 'root',
name: 'src',
type: 'folder',
children: [
{
id: 'file-1',
name: 'script.lua',
type: 'file',
content: `-- Welcome to AeThex Studio!
-- Write your Roblox Lua code here
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game!")
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = leaderstats
end)`,
},
],
},
]);
const [openFiles, setOpenFiles] = useState<FileNode[]>([]);
const [activeFileId, setActiveFileId] = useState<string>('file-1');
const handleTemplateSelect = (templateCode: string) => {
setCode(templateCode);
setCurrentCode(templateCode);
};
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 handleFileCreate = (name: string, parentId?: string) => {
const newFile: FileNode = {
id: `file-${Date.now()}`,
name: name.endsWith('.lua') ? name : `${name}.lua`,
type: 'file',
content: '-- New file\n',
};
setFiles((prev) => {
const addToFolder = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === 'root' && !parentId) {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.id === parentId && node.type === 'folder') {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.children) {
return { ...node, children: addToFolder(node.children) };
}
return node;
});
};
return addToFolder(prev || []);
});
captureEvent('file_create', { name, parentId });
toast.success(`Created ${newFile.name}`);
};
const handleFileRename = (id: string, newName: string) => {
setFiles((prev) => {
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;
});
};
return rename(prev || []);
});
};
const handleFileDelete = (id: string) => {
setFiles((prev) => {
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;
});
};
return deleteNode(prev || []);
});
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
if (activeFileId === id) {
setActiveFileId((openFiles || [])[0]?.id || '');
}
captureEvent('file_delete', { id });
};
const handleFileClose = (id: string) => {
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
if (activeFileId === id) {
const remaining = (openFiles || []).filter((f) => f.id !== id);
setActiveFileId(remaining[0]?.id || '');
}
};
const handleCreateProject = (config: ProjectConfig) => {
const projectFiles: FileNode[] = [
{
id: 'root',
name: config.name,
type: 'folder',
children: [
{
id: `file-${Date.now()}`,
name: 'main.lua',
type: 'file',
content: `-- ${config.name}\n-- Template: ${config.template}\n\nprint("Project initialized!")`,
},
],
},
];
setFiles(projectFiles);
setOpenFiles([]);
setActiveFileId('');
};
// Example user stub for profile
const demoUser = user || {
login: 'demo-user',
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
email: 'demo@aethex.com',
};
return (
<div className="h-screen flex flex-col bg-background text-foreground">
<Toolbar
code={currentCode}
onTemplatesClick={() => setShowTemplates(true)}
onPreviewClick={() => setShowPreview(true)}
onNewProjectClick={() => setShowNewProject(true)}
/>
<div className="flex-1 overflow-hidden flex flex-col">
{isMobile ? (
<Tabs defaultValue="editor" className="h-full flex flex-col">
<TabsList className="w-full rounded-none border-b border-border">
<TabsTrigger value="files" className="flex-1">Files</TabsTrigger>
<TabsTrigger value="editor" className="flex-1">Editor</TabsTrigger>
<TabsTrigger value="ai" className="flex-1">AI</TabsTrigger>
<TabsTrigger value="education" className="flex-1">Learn</TabsTrigger>
</TabsList>
<TabsContent value="files" className="flex-1 m-0">
<FileTree
files={files || []}
onFileSelect={handleFileSelect}
onFileCreate={handleFileCreate}
onFileRename={handleFileRename}
onFileDelete={handleFileDelete}
selectedFileId={activeFileId}
/>
</TabsContent>
<TabsContent value="editor" className="flex-1 m-0 flex flex-col">
<FileTabs
openFiles={openFiles || []}
activeFileId={activeFileId}
onFileSelect={handleFileSelect}
onFileClose={handleFileClose}
/>
<div className="flex-1">
<CodeEditor onCodeChange={setCurrentCode} />
</div>
</TabsContent>
<TabsContent value="ai" className="flex-1 m-0">
<AIChat currentCode={currentCode} />
</TabsContent>
<TabsContent value="education" className="flex-1 m-0">
<EducationPanel />
</TabsContent>
</Tabs>
) : (
<>
<div className="flex-1 overflow-hidden">
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={15} minSize={10} maxSize={25}>
<FileTree
files={files || []}
onFileSelect={handleFileSelect}
onFileCreate={handleFileCreate}
onFileRename={handleFileRename}
onFileDelete={handleFileDelete}
selectedFileId={activeFileId}
/>
</ResizablePanel>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
<ResizablePanel defaultSize={55} minSize={30}>
<div className="h-full flex flex-col">
<FileTabs
openFiles={openFiles || []}
activeFileId={activeFileId}
onFileSelect={handleFileSelect}
onFileClose={handleFileClose}
/>
<div className="flex-1">
<CodeEditor onCodeChange={setCurrentCode} />
</div>
</div>
</ResizablePanel>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
<ResizablePanel defaultSize={20} minSize={15}>
<EducationPanel />
</ResizablePanel>
<ResizablePanel defaultSize={15} minSize={10}>
<AIChat currentCode={currentCode} />
</ResizablePanel>
</ResizablePanelGroup>
</div>
<ConsolePanel />
</>
)}
{/* Unified feature tabs for all major panels */}
<div className="w-full border-t border-border mt-4">
<ExtraTabs user={demoUser} />
</div>
</div>
{showTemplates && (
<TemplatesDrawer
onSelectTemplate={handleTemplateSelect}
onClose={() => setShowTemplates(false)}
/>
)}
<PreviewModal
open={showPreview}
onClose={() => setShowPreview(false)}
code={currentCode}
/>
<NewProjectModal
open={showNewProject}
onClose={() => setShowNewProject(false)}
onCreateProject={handleCreateProject}
/>
<WelcomeDialog />
{!user && (
<Button
variant="secondary"
className="fixed top-4 right-4 z-50"
onClick={() => setShowPassportLogin(true)}
>
Sign In
</Button>
)}
{user && (
<div className="fixed top-4 right-4 z-50 flex items-center gap-2 bg-card px-3 py-1 rounded shadow">
<img src={user.avatarUrl} alt={user.login} className="h-6 w-6 rounded-full" />
<span className="text-xs font-medium">{user.login}</span>
<Button variant="ghost" size="sm" onClick={handleSignOut}>
Sign Out
</Button>
</div>
)}
<PassportLogin
open={showPassportLogin}
onClose={() => setShowPassportLogin(false)}
onLoginSuccess={handleLoginSuccess}
/>
<Toaster position="bottom-right" theme="dark" />
</div>
);
}
export default App;