mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
282 lines
9 KiB
TypeScript
282 lines
9 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { useLabTerminal } from "@/hooks/use-lab-terminal";
|
|
import { ChevronDown, Plus, X, Copy, Download, Settings, Play, Loader2 } from "lucide-react";
|
|
import { useLocation } from "wouter";
|
|
|
|
interface File {
|
|
id: string;
|
|
name: string;
|
|
content: string;
|
|
language: string;
|
|
}
|
|
|
|
export default function Lab() {
|
|
const [files, setFiles] = useState<File[]>([
|
|
{
|
|
id: "1",
|
|
name: "registry.ts",
|
|
language: "typescript",
|
|
content: `interface Architect {
|
|
id: string;
|
|
level: number;
|
|
xp: number;
|
|
verified: boolean;
|
|
}
|
|
|
|
class MetaverseRegistry {
|
|
private architects: Map<string, Architect>;
|
|
|
|
constructor() {
|
|
this.architects = new Map();
|
|
}
|
|
|
|
registerArchitect(architect: Architect): void {
|
|
this.architects.set(architect.id, architect);
|
|
}
|
|
|
|
getArchitect(id: string): Architect | undefined {
|
|
return this.architects.get(id);
|
|
}
|
|
|
|
getAllArchitects(): Architect[] {
|
|
return Array.from(this.architects.values());
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "README.txt",
|
|
language: "text",
|
|
content: `The Lab - Code Editor & Registry
|
|
|
|
Welcome to AeThex-OS Lab. This is where you can:
|
|
- Write and edit code
|
|
- Manage TypeScript interfaces and classes
|
|
- Build the MetaverseRegistry
|
|
- Compile and test your creations
|
|
|
|
Get started by creating new files or editing existing ones.
|
|
`,
|
|
},
|
|
]);
|
|
|
|
const [activeFileId, setActiveFileId] = useState(files[0].id);
|
|
const [newFileName, setNewFileName] = useState("");
|
|
const editorRef = useRef<HTMLTextAreaElement>(null);
|
|
const { executeCode, isExecuting, lastExecution } = useLabTerminal();
|
|
const [, setLocation] = useLocation();
|
|
|
|
const activeFile = files.find((f) => f.id === activeFileId);
|
|
|
|
const handleCreateFile = () => {
|
|
if (!newFileName.trim()) return;
|
|
const newFile: File = {
|
|
id: Date.now().toString(),
|
|
name: newFileName,
|
|
content: "",
|
|
language: newFileName.endsWith(".ts") ? "typescript" : "text",
|
|
};
|
|
setFiles([...files, newFile]);
|
|
setActiveFileId(newFile.id);
|
|
setNewFileName("");
|
|
};
|
|
|
|
const handleDeleteFile = (id: string) => {
|
|
if (files.length === 1) return;
|
|
const filtered = files.filter((f) => f.id !== id);
|
|
setFiles(filtered);
|
|
setActiveFileId(filtered[0].id);
|
|
};
|
|
|
|
const handleUpdateContent = (content: string) => {
|
|
setFiles(
|
|
files.map((f) =>
|
|
f.id === activeFileId ? { ...f, content } : f
|
|
)
|
|
);
|
|
};
|
|
|
|
const handleDownload = () => {
|
|
if (!activeFile) return;
|
|
const element = document.createElement("a");
|
|
element.setAttribute(
|
|
"href",
|
|
`data:text/plain;charset=utf-8,${encodeURIComponent(activeFile.content)}`
|
|
);
|
|
element.setAttribute("download", activeFile.name);
|
|
element.style.display = "none";
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
};
|
|
|
|
const handleCopy = () => {
|
|
if (!activeFile) return;
|
|
navigator.clipboard.writeText(activeFile.content);
|
|
};
|
|
|
|
const handleRun = async () => {
|
|
if (!activeFile) return;
|
|
await executeCode(activeFile.content, activeFile.language);
|
|
// Open terminal to show results
|
|
setLocation('/terminal');
|
|
};
|
|
|
|
return (
|
|
<div className="h-screen w-full bg-gradient-to-br from-slate-900 to-slate-800 flex flex-col">
|
|
{/* Header */}
|
|
<div className="bg-slate-950 border-b border-slate-700 px-4 py-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-cyan-400 text-2xl"></></div>
|
|
<h1 className="text-xl font-bold text-white">The Lab</h1>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button size="sm" variant="ghost">
|
|
<Settings className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* File Explorer */}
|
|
<div className="w-64 bg-slate-900 border-r border-slate-700 flex flex-col">
|
|
<div className="p-4 border-b border-slate-700">
|
|
<h2 className="text-sm font-semibold text-slate-300 mb-3">FILES</h2>
|
|
<div className="flex gap-2 mb-3">
|
|
<Input
|
|
placeholder="New file..."
|
|
value={newFileName}
|
|
onChange={(e) => setNewFileName(e.target.value)}
|
|
onKeyPress={(e) => {
|
|
if (e.key === "Enter") handleCreateFile();
|
|
}}
|
|
className="h-8 text-xs bg-slate-800 border-slate-700"
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleCreateFile}
|
|
className="bg-cyan-600 hover:bg-cyan-700"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
|
{files.map((file) => (
|
|
<div
|
|
key={file.id}
|
|
onClick={() => setActiveFileId(file.id)}
|
|
className={`px-4 py-2 flex items-center justify-between cursor-pointer border-l-2 transition-colors ${
|
|
activeFileId === file.id
|
|
? "bg-slate-800 border-cyan-400 text-cyan-400"
|
|
: "border-transparent text-slate-400 hover:bg-slate-800"
|
|
}`}
|
|
>
|
|
<span className="text-sm truncate">{file.name}</span>
|
|
{files.length > 1 && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDeleteFile(file.id);
|
|
}}
|
|
className="hover:text-red-400"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Editor */}
|
|
{activeFile && (
|
|
<div className="flex-1 flex flex-col bg-slate-800">
|
|
{/* Tab Bar */}
|
|
<div className="bg-slate-900 border-b border-slate-700 px-4 py-2 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-slate-400">
|
|
{activeFile.name}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-slate-500">
|
|
{activeFile.language.toUpperCase()} • UTF-8 • Spaces: 2
|
|
</div>
|
|
</div>
|
|
|
|
{/* Code Editor */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Line Numbers */}
|
|
<div className="bg-slate-900 border-r border-slate-700 px-3 py-4 text-right select-none">
|
|
{activeFile.content.split("\n").map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="text-slate-600 text-xs font-mono leading-6"
|
|
>
|
|
{i + 1}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Editor Content */}
|
|
<textarea
|
|
ref={editorRef}
|
|
value={activeFile.content}
|
|
onChange={(e) => handleUpdateContent(e.target.value)}
|
|
className="flex-1 bg-slate-800 text-slate-100 p-4 font-mono text-sm leading-6 resize-none focus:outline-none border-none focus:ring-0"
|
|
spellCheck="false"
|
|
/>
|
|
</div>
|
|
|
|
{/* Status Bar */}
|
|
<div className="bg-blue-600 px-4 py-2 flex items-center justify-between text-xs text-white">
|
|
<div>
|
|
Ln {activeFile.content.split("\n").length}, Col 1
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleRun}
|
|
disabled={isExecuting}
|
|
className="text-white hover:bg-blue-700"
|
|
>
|
|
{isExecuting ? (
|
|
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
|
) : (
|
|
<Play className="w-3 h-3 mr-1" />
|
|
)}
|
|
{isExecuting ? "Running..." : "Run"}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleCopy}
|
|
className="text-white hover:bg-blue-700"
|
|
>
|
|
<Copy className="w-3 h-3 mr-1" />
|
|
Copy
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleDownload}
|
|
className="text-white hover:bg-blue-700"
|
|
>
|
|
<Download className="w-3 h-3 mr-1" />
|
|
Download
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|