Spaces:
Running
Running
| import { useState, useEffect, useRef } from "react"; | |
| import Editor from "@monaco-editor/react"; | |
| import { askAgent } from "./agent/assistant"; | |
| import { runCode } from "./agent/runner"; | |
| import { | |
| loadTree, | |
| saveTree, | |
| addFile, | |
| addFolder, | |
| renameNode, | |
| deleteNode, | |
| getNodeByPath, | |
| updateFileContent, | |
| searchTree, | |
| } from "./fileStore"; | |
| import { downloadProjectZip } from "./zipExport"; | |
| import { parseProblems } from "./problemParser"; | |
| import "./App.css"; | |
| // =================== SUPPORTED LANGUAGES =================== | |
| const LANGUAGE_OPTIONS = [ | |
| { id: "python", ext: ".py", icon: "π", monaco: "python" }, | |
| { id: "javascript", ext: ".js", icon: "π¨", monaco: "javascript" }, | |
| { id: "typescript", ext: ".ts", icon: "π¦", monaco: "typescript" }, | |
| { id: "cpp", ext: ".cpp", icon: "π ", monaco: "cpp" }, | |
| { id: "c", ext: ".c", icon: "π·", monaco: "c" }, | |
| { id: "java", ext: ".java", icon: "β", monaco: "java" }, | |
| { id: "html", ext: ".html", icon: "π", monaco: "html" }, | |
| { id: "css", ext: ".css", icon: "π¨", monaco: "css" }, | |
| { id: "json", ext: ".json", icon: "π§Ύ", monaco: "json" }, | |
| ]; | |
| const RUNNABLE_LANGS = ["python", "javascript", "java"]; | |
| // =================== APP =================== | |
| function App() { | |
| const [tree, setTree] = useState(loadTree()); | |
| const [activePath, setActivePath] = useState("main.py"); | |
| const [output, setOutput] = useState(""); | |
| const [prompt, setPrompt] = useState(""); | |
| const [explanation, setExplanation] = useState(""); | |
| const [stdin, setStdin] = useState(""); | |
| const [problems, setProblems] = useState([]); | |
| const [theme, setTheme] = useState("vs-dark"); | |
| const [searchOpen, setSearchOpen] = useState(false); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [aiSuggestions, setAiSuggestions] = useState([]); | |
| const [contextMenu, setContextMenu] = useState(null); // right-click menu | |
| const editorRef = useRef(null); | |
| const currentFile = getNodeByPath(tree, activePath); | |
| const langMeta = | |
| LANGUAGE_OPTIONS.find((l) => currentFile?.name.endsWith(l.ext)) || | |
| LANGUAGE_OPTIONS[0]; | |
| // Save after tree change | |
| useEffect(() => { | |
| saveTree(tree); | |
| }, [tree]); | |
| // =================== FILE ACTIONS =================== | |
| const handleNewFile = () => { | |
| const name = window.prompt("Filename (with extension):"); | |
| if (!name) return; | |
| setTree(addFile(tree, name)); | |
| }; | |
| const handleNewFolder = () => { | |
| const name = window.prompt("Folder name:"); | |
| if (!name) return; | |
| setTree(addFolder(tree, name)); | |
| }; | |
| const handleRename = () => { | |
| if (!activePath) return; | |
| const newName = window.prompt("New name:", currentFile.name); | |
| if (!newName) return; | |
| setTree(renameNode(tree, activePath, newName)); | |
| setActivePath(newName); | |
| }; | |
| const handleDelete = () => { | |
| if (!activePath) return; | |
| setTree(deleteNode(tree, activePath)); | |
| setActivePath(""); // unselect | |
| }; | |
| const downloadFile = () => { | |
| if (!currentFile?.content) return; | |
| const blob = new Blob([currentFile.content], { | |
| type: "text/plain;charset=utf-8", | |
| }); | |
| const a = document.createElement("a"); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = currentFile.name; | |
| a.click(); | |
| }; | |
| // =================== RUN =================== | |
| const handleRun = async () => { | |
| if (!currentFile?.content) return; | |
| const selectedLang = langMeta.id; | |
| if (!RUNNABLE_LANGS.includes(selectedLang)) { | |
| setOutput(`β οΈ Cannot run ${selectedLang}.`); | |
| return; | |
| } | |
| const res = await runCode(currentFile.content, selectedLang, stdin); | |
| setOutput(res.output || ""); | |
| setProblems(res.error ? parseProblems(res.output) : []); | |
| }; | |
| // =================== AI =================== | |
| const handleAskFix = async () => { | |
| if (!currentFile) return; | |
| const userHint = prompt.trim() ? `User request: ${prompt}` : ""; | |
| const reply = await askAgent( | |
| `Improve, debug, or refactor this ${langMeta.id} file. | |
| ${userHint} | |
| Return ONLY updated code, no explanation. | |
| CODE: | |
| ${currentFile.content}` | |
| ); | |
| updateActiveFileContent(reply); | |
| }; | |
| const handleExplainSelection = async () => { | |
| if (!currentFile) return; | |
| const editor = editorRef.current; | |
| const selected = editor | |
| .getModel() | |
| .getValueInRange(editor.getSelection()); | |
| const code = selected.trim() || currentFile.content; | |
| const userHint = prompt.trim() | |
| ? `Focus on: ${prompt}` | |
| : "Give a clear and simple explanation."; | |
| const reply = await askAgent( | |
| `Explain what this ${langMeta.id} code does, any risks, and improvements. | |
| ${userHint} | |
| CODE: | |
| ${code}` | |
| ); | |
| setExplanation(reply); | |
| }; | |
| const updateActiveFileContent = (value) => { | |
| const updated = updateFileContent(tree, activePath, value); | |
| setTree(updated); | |
| }; | |
| // ===== AI Autocomplete Suggestions (Popup) ===== | |
| const fetchAiSuggestions = async (code) => { | |
| if (!code?.trim()) return; | |
| const reply = await askAgent( | |
| `Suggest possible next lines for continuation. Return 3 short snippets.\n${code}` | |
| ); | |
| setAiSuggestions(reply.split("\n").filter((l) => l.trim())); | |
| }; | |
| // =================== SEARCH =================== | |
| const handleSearchToggle = () => setSearchOpen(!searchOpen); | |
| const handleSearchNow = () => { | |
| if (!searchQuery) return; | |
| alert( | |
| `Found occurrences:\n${JSON.stringify( | |
| searchTree(tree, searchQuery), | |
| null, | |
| 2 | |
| )}` | |
| ); | |
| }; | |
| // =================== UI =================== | |
| return ( | |
| <div | |
| className={`ide-root ${ | |
| theme === "vs-dark" ? "ide-dark" : "ide-light" | |
| }`} | |
| > | |
| {/* ==== TOP BAR ==== */} | |
| <div className="ide-menubar"> | |
| <div className="ide-menubar-left"> | |
| <span className="ide-logo">βοΈ DevMate IDE</span> | |
| <button onClick={handleNewFile}>π New File</button> | |
| <button onClick={handleNewFolder}>π New Folder</button> | |
| <button onClick={handleRename}>βοΈ Rename</button> | |
| <button onClick={downloadFile}>π₯ Download</button> | |
| <button onClick={downloadProjectZip}>π¦ ZIP</button> | |
| </div> | |
| <div className="ide-menubar-right"> | |
| <button onClick={handleSearchToggle}>π Search</button> | |
| <button onClick={handleRun}>βΆ Run</button> | |
| <button onClick={handleAskFix}>π€ Fix</button> | |
| <button onClick={handleExplainSelection}>π Explain</button> | |
| <button | |
| onClick={() => | |
| setTheme(theme === "vs-dark" ? "light" : "vs-dark") | |
| } | |
| > | |
| {theme === "vs-dark" ? "βοΈ" : "π"} | |
| </button> | |
| </div> | |
| </div> | |
| {/* ==== BODY ==== */} | |
| <div className="ide-body"> | |
| {/* ==== TREE VIEW (LEFT) ==== */} | |
| <div className="ide-sidebar"> | |
| {renderTree(tree, setActivePath, setContextMenu)} | |
| </div> | |
| {/* ==== MAIN CENTER (EDITOR + OUTPUT) ==== */} | |
| <div className="ide-main"> | |
| <div className="ide-editor-wrapper"> | |
| <Editor | |
| height="100%" | |
| theme={theme} | |
| language={langMeta.monaco} | |
| value={currentFile?.content || ""} | |
| onChange={updateActiveFileContent} | |
| onMount={(editor) => (editorRef.current = editor)} | |
| onBlur={() => fetchAiSuggestions(currentFile?.content)} | |
| /> | |
| </div> | |
| {/* AI Suggestions Tooltip */} | |
| {aiSuggestions.length > 0 && ( | |
| <div className="ai-popup"> | |
| {aiSuggestions.map((s, i) => ( | |
| <div | |
| key={i} | |
| className="ai-suggest" | |
| onClick={() => | |
| updateActiveFileContent( | |
| (currentFile?.content || "") + "\n" + s | |
| ) | |
| } | |
| > | |
| {s} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Bottom Panels: Output + stdin + Problems */} | |
| <div className="ide-panels"> | |
| <pre className="ide-output">{output}</pre> | |
| <input | |
| className="ide-input-box" | |
| placeholder="Program input..." | |
| value={stdin} | |
| onChange={(e) => setStdin(e.target.value)} | |
| /> | |
| {problems.length > 0 && ( | |
| <div className="ide-problems-panel"> | |
| <div>π¨ Problems ({problems.length})</div> | |
| {problems.map((p, i) => ( | |
| <div key={i}> | |
| {p.file}:{p.line} β {p.message} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* ==== RIGHT AI PANEL ==== */} | |
| <div className="ide-right-panel"> | |
| <div className="ide-ai-header">π€ AI Assistant</div> | |
| <div className="ide-ai-section"> | |
| <label className="ide-ai-label">Instruction</label> | |
| <textarea | |
| className="ide-agent-textarea" | |
| placeholder="Ask the AI (e.g., 'optimize this function', 'add comments', 'convert to JS'...)" | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| /> | |
| </div> | |
| <div className="ide-ai-buttons"> | |
| <button onClick={handleAskFix}>π‘ Apply Fix</button> | |
| <button onClick={handleExplainSelection}>π Explain Code</button> | |
| </div> | |
| {explanation && ( | |
| <div className="ide-ai-section"> | |
| <label className="ide-ai-label">Explanation</label> | |
| <div className="ide-explain">{explanation}</div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* ==== SEARCH PANEL ==== */} | |
| {searchOpen && ( | |
| <div className="search-dialog"> | |
| <input | |
| placeholder="Search text..." | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| <button onClick={handleSearchNow}>Search</button> | |
| </div> | |
| )} | |
| {/* ==== RIGHT-CLICK CONTEXT MENU ==== */} | |
| {contextMenu && ( | |
| <div | |
| className="ide-context-menu" | |
| style={{ top: contextMenu.y, left: contextMenu.x }} | |
| onMouseLeave={() => setContextMenu(null)} | |
| > | |
| <div onClick={handleRename}>βοΈ Rename</div> | |
| <div onClick={handleDelete}>π Delete</div> | |
| <div onClick={downloadFile}>π₯ Download</div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| // ======================================================= | |
| // Tree Renderer UI | |
| // ======================================================= | |
| function renderTree(node, setActivePath, setContextMenu, depth = 0) { | |
| return ( | |
| <div key={node.name} style={{ paddingLeft: depth * 10 }}> | |
| <div | |
| className={`tree-item ${node.type}`} | |
| onClick={() => node.type === "file" && setActivePath(node.path)} | |
| onContextMenu={(e) => { | |
| e.preventDefault(); | |
| setContextMenu({ x: e.pageX, y: e.pageY, file: node.path }); | |
| }} | |
| > | |
| {node.type === "folder" ? "π" : "π"} {node.name} | |
| </div> | |
| {node.children && | |
| node.children.map((child) => | |
| renderTree(child, setActivePath, setContextMenu, depth + 1) | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default App; | |