modified: components/StudioEditor.tsx

This commit is contained in:
Anderson 2026-01-28 23:31:17 +00:00 committed by GitHub
parent ea5ba62c54
commit a8b2ffc3fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 356 additions and 57 deletions

View file

@ -6,7 +6,7 @@ import { useEditorStore, FileNode } from '@/store/editor-store';
import { cn, getFileIcon, getPlatformIcon } from '@/lib/utils';
export function FileTree() {
const { files, openFile } = useEditorStore();
const { files, openFile, moveFile } = useEditorStore();
const [expandedFolders, setExpandedFolders] = React.useState<Set<string>>(
new Set(['roblox', 'web', 'mobile', 'desktop', 'shared'])
);
@ -29,7 +29,10 @@ export function FileTree() {
return (
<div key={node.id} draggable onDragStart={e => e.dataTransfer.setData('fileId', node.id)} onDrop={e => {
e.preventDefault();
// moveFile functionality is not implemented
const fileId = e.dataTransfer.getData('fileId');
if (fileId && fileId !== node.id && isFolder) {
moveFile(fileId, node.id);
}
}} onDragOver={e => isFolder && e.preventDefault()}>
<div
className={cn(

View file

@ -2,19 +2,19 @@
import { useEditorStore } from "../store/editor-zustand";
function StudioEditor() {
const openTabs = useEditorStore((s: any) => s.openTabs);
const activeTabId = useEditorStore((s: any) => s.activeTabId);
const setActiveTab = useEditorStore((s: any) => s.setActiveTab);
const closeTab = useEditorStore((s: any) => s.closeTab);
const openTabs = useEditorStore((s) => s.openTabs);
const activeTabId = useEditorStore((s) => s.activeTabId);
const setActiveTab = useEditorStore((s) => s.setActiveTab);
const closeTab = useEditorStore((s) => s.closeTab);
// Find active file
const activeFile = openTabs.find((f: any) => f.id === activeTabId);
const activeFile = openTabs.find(f => f.id === activeTabId);
return (
<div className="editor-area">
<div className="editor-tabs">
{openTabs.length === 0 && (
<div className="editor-tab empty">No files open</div>
)}
{openTabs.map((file: any) => (
{openTabs.map((file) => (
<div
key={file.id}
className={"editor-tab" + (activeTabId === file.id ? " active" : "")}
@ -33,7 +33,7 @@ function StudioEditor() {
</div>
<div className="editor-content">
{activeFile ? (
activeFile.content.split("\n").map((line: string, i: number) => (
activeFile.content.split("\n").map((line, i) => (
<div className="code-line" key={i}>
<div className="line-number">{i + 1}</div>
<div className="line-content">{line}</div>

View file

@ -1,59 +1,358 @@
"use client";
import React, { useState } from "react";
import StudioSidebar from "./components/StudioSidebar";
import StudioEditor from "./components/StudioEditor";
import StudioBottomPanel from "./components/StudioBottomPanel";
import StudioRightPanel from "./components/StudioRightPanel";
import StudioNetworkViz from "./components/StudioNetworkViz";
import TemplatesDrawer from "./components/TemplatesDrawer";
import PreviewModal from "./components/PreviewModal";
import NewProjectModal from "./components/NewProjectModal";
import TranslationPanel from "./components/TranslationPanel";
import PassportLogin from "./components/PassportLogin";
import Toaster from "./components/ui/toaster";
import CommandPalette from "./components/ui/CommandPalette";
import { toast } from "./lib/toast";
import { captureEvent } from "./lib/captureEvent";
import Suspense from "./components/Suspense";
import StudioLayout from "../components/StudioLayout";
// ...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() {
// Minimal state stubs
const [user] = useState<any>(null);
const [showPassportLogin, setShowPassportLogin] = useState(false);
const [showNewProject, setShowNewProject] = useState(false);
// --- 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 [showTranslation, setShowTranslation] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [showNewProject, setShowNewProject] = useState(false);
const [showFileSearch, setShowFileSearch] = useState(false);
const [showCommandPalette, setShowCommandPalette] = useState(false);
const [consoleCollapsed, setConsoleCollapsed] = useState(false);
const [openFiles, setOpenFiles] = useState<any[]>([]);
const [activeFileId, setActiveFileId] = useState<string>("");
const [code, setCode] = useState<string>("");
const [currentCode, setCurrentCode] = useState<string>("");
const [showPreview, setShowPreview] = 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>('');
// Handler stubs
const handleCodeChange = (c: string) => setCode(c);
const handleFileSelect = (id: string) => setActiveFileId(id);
const handleFileClose = (id: string) => setOpenFiles(files => files.filter(f => f.id !== id));
const handleTemplateSelect = () => {};
const handleFileRename = () => {};
const handleFileDelete = () => {};
const handleFileMove = () => {};
// --- 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 (
<StudioLayout>
<Toaster />
<Suspense>
{showTemplates && <TemplatesDrawer onSelectTemplate={handleTemplateSelect} onClose={() => setShowTemplates(false)} currentPlatform={"roblox"} />}
<>
<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={"roblox"} />}
{showTranslation && <TranslationPanel isOpen={showTranslation} onClose={() => setShowTranslation(false)} currentCode={currentCode} currentPlatform={currentPlatform} />}
{showPassportLogin && <PassportLogin open={showPassportLogin} onClose={() => setShowPassportLogin(false)} onLoginSuccess={() => {}} />}
</Suspense>
<CommandPalette />
</StudioLayout>
<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!') },
]}
/>
</>
);
}

View file

@ -20,10 +20,7 @@
}
],
"paths": {
"@/*": ["src/*"],
"@/store/*": ["store/*"],
"@/lib/*": ["src/lib/*"],
"@/hooks/*": ["src/hooks/*"]
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],