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"; | |
| // Supported languages in the IDE | |
| const LANGUAGE_OPTIONS = [ | |
| { id: "python", label: "Python", ext: ".py", icon: "🐍", monaco: "python" }, | |
| { id: "javascript", label: "JavaScript", ext: ".js", icon: "🟨", monaco: "javascript" }, | |
| { id: "typescript", label: "TypeScript", ext: ".ts", icon: "🟦", monaco: "typescript" }, | |
| { id: "cpp", label: "C++", ext: ".cpp", icon: "💠", monaco: "cpp" }, | |
| { id: "c", label: "C", ext: ".c", icon: "🔷", monaco: "c" }, | |
| { id: "java", label: "Java", ext: ".java", icon: "☕", monaco: "java" }, | |
| { id: "html", label: "HTML", ext: ".html", icon: "🌐", monaco: "html" }, | |
| { id: "css", label: "CSS", ext: ".css", icon: "🎨", monaco: "css" }, | |
| { id: "json", label: "JSON", ext: ".json", icon: "🧾", monaco: "json" }, | |
| ]; | |
| // Languages we actually can run on the backend right now | |
| const RUNNABLE_LANGS = ["python", "javascript"]; | |
| // LocalStorage key | |
| const STORAGE_KEY = "devmate_ide_files"; | |
| function App() { | |
| const [files, setFiles] = useState({ | |
| "main.py": { | |
| language: "python", | |
| content: "# Python\nprint('Hello from IDE')", | |
| }, | |
| "script.js": { | |
| language: "javascript", | |
| content: "console.log('Hello from JS');", | |
| }, | |
| }); | |
| const [activeFile, setActiveFile] = useState("main.py"); | |
| const [output, setOutput] = useState(""); | |
| const [prompt, setPrompt] = useState(""); | |
| const [explanation, setExplanation] = useState(""); | |
| const [theme, setTheme] = useState("vs-dark"); // "vs-dark" | "light" | |
| const [openMenu, setOpenMenu] = useState(null); // "file" | "run" | "ai" | null | |
| const editorRef = useRef(null); | |
| const currentFile = files[activeFile]; | |
| const currentLangId = currentFile?.language || "python"; | |
| const langMeta = | |
| LANGUAGE_OPTIONS.find((l) => l.id === currentLangId) || LANGUAGE_OPTIONS[0]; | |
| // ----- LocalStorage: load on mount ----- | |
| useEffect(() => { | |
| try { | |
| const saved = localStorage.getItem(STORAGE_KEY); | |
| if (saved) { | |
| const parsed = JSON.parse(saved); | |
| if (parsed && typeof parsed === "object") { | |
| setFiles(parsed.files || files); | |
| setActiveFile(parsed.activeFile || Object.keys(parsed.files || files)[0]); | |
| } | |
| } | |
| } catch (e) { | |
| console.warn("Failed to load saved files:", e); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []); | |
| // ----- LocalStorage: save whenever files/activeFile change ----- | |
| useEffect(() => { | |
| try { | |
| localStorage.setItem( | |
| STORAGE_KEY, | |
| JSON.stringify({ files, activeFile }) | |
| ); | |
| } catch (e) { | |
| console.warn("Failed to save files:", e); | |
| } | |
| }, [files, activeFile]); | |
| const updateActiveFileContent = (value) => { | |
| if (!activeFile) return; | |
| setFiles((prev) => ({ | |
| ...prev, | |
| [activeFile]: { | |
| ...prev[activeFile], | |
| content: value ?? "", | |
| }, | |
| })); | |
| }; | |
| const handleRun = async () => { | |
| if (!currentFile) return; | |
| if (!RUNNABLE_LANGS.includes(currentLangId)) { | |
| setOutput( | |
| `⚠️ Run is only supported for: ${RUNNABLE_LANGS.join( | |
| ", " | |
| )}.\nSelected language: ${currentLangId}` | |
| ); | |
| return; | |
| } | |
| try { | |
| const res = await runCode(currentFile.content, currentLangId); | |
| setOutput(res.output ?? ""); | |
| } catch (err) { | |
| setOutput(`Error running code: ${String(err)}`); | |
| } | |
| setOpenMenu(null); | |
| }; | |
| const handleAskFix = async () => { | |
| if (!currentFile) return; | |
| const fullPrompt = ` | |
| You are an AI coding assistant inside an online IDE. | |
| Improve, debug, or refactor the following ${currentLangId} code. | |
| Return ONLY the updated code, with no explanation. | |
| User request/hint (optional): | |
| ${prompt || "(no extra hint)"} | |
| Code: | |
| ${currentFile.content} | |
| `.trim(); | |
| const reply = await askAgent(fullPrompt, [ | |
| { role: "user", content: currentFile.content }, | |
| ]); | |
| updateActiveFileContent(reply); | |
| setOpenMenu(null); | |
| }; | |
| const handleExplainSelection = async () => { | |
| if (!currentFile) return; | |
| const editor = editorRef.current; | |
| let selectedCode = ""; | |
| if (editor) { | |
| const model = editor.getModel(); | |
| const selection = editor.getSelection(); | |
| if (model && selection) { | |
| selectedCode = model.getValueInRange(selection); | |
| } | |
| } | |
| if (!selectedCode.trim()) { | |
| selectedCode = currentFile.content; | |
| } | |
| const explainPrompt = ` | |
| You are an AI code explainer inside an IDE. | |
| Explain clearly and concisely what the following ${currentLangId} code does, | |
| including edge cases and any potential issues. | |
| Code: | |
| ${selectedCode} | |
| `.trim(); | |
| const reply = await askAgent(explainPrompt, [ | |
| { role: "user", content: selectedCode }, | |
| ]); | |
| setExplanation(reply); | |
| setOpenMenu(null); | |
| }; | |
| const handleNewFile = () => { | |
| const base = "untitled"; | |
| let idx = 1; | |
| let name = `${base}${idx}${langMeta.ext}`; | |
| while (files[name]) { | |
| idx += 1; | |
| name = `${base}${idx}${langMeta.ext}`; | |
| } | |
| setFiles((prev) => ({ | |
| ...prev, | |
| [name]: { | |
| language: currentLangId, | |
| content: `// ${langMeta.label} file\n`, | |
| }, | |
| })); | |
| setActiveFile(name); | |
| setOpenMenu(null); | |
| }; | |
| const handleDeleteFile = () => { | |
| if (!activeFile) return; | |
| if (Object.keys(files).length === 1) return; // keep at least one | |
| const newFiles = { ...files }; | |
| delete newFiles[activeFile]; | |
| const remaining = Object.keys(newFiles); | |
| setFiles(newFiles); | |
| setActiveFile(remaining[0] || ""); | |
| }; | |
| const handleChangeLanguage = (langId) => { | |
| setFiles((prev) => ({ | |
| ...prev, | |
| [activeFile]: { | |
| ...prev[activeFile], | |
| language: langId, | |
| }, | |
| })); | |
| }; | |
| const toggleTheme = () => { | |
| setTheme((prev) => (prev === "vs-dark" ? "light" : "vs-dark")); | |
| }; | |
| const toggleMenu = (menuName) => { | |
| setOpenMenu((prev) => (prev === menuName ? null : menuName)); | |
| }; | |
| const handleSaveLocal = () => { | |
| // Already persisted automatically; this is more like a user-triggered "save" action | |
| try { | |
| localStorage.setItem( | |
| STORAGE_KEY, | |
| JSON.stringify({ files, activeFile }) | |
| ); | |
| setOutput("💾 Saved project to browser storage."); | |
| } catch (e) { | |
| setOutput(`Failed to save: ${String(e)}`); | |
| } | |
| setOpenMenu(null); | |
| }; | |
| const handleDownloadFile = () => { | |
| if (!currentFile || !activeFile) return; | |
| const blob = new Blob([currentFile.content], { type: "text/plain;charset=utf-8" }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = activeFile; | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| setOpenMenu(null); | |
| }; | |
| return ( | |
| <div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}> | |
| {/* Top Menu Bar */} | |
| <div className="ide-menubar"> | |
| <div className="ide-menubar-left"> | |
| <span className="ide-logo">⚙️ DevMate IDE</span> | |
| {/* File menu */} | |
| <div className="ide-menu-wrapper"> | |
| <button | |
| className="ide-menu-item" | |
| onClick={() => toggleMenu("file")} | |
| > | |
| File ▾ | |
| </button> | |
| {openMenu === "file" && ( | |
| <div className="ide-menu-dropdown"> | |
| <button onClick={handleNewFile}>New File</button> | |
| <button onClick={handleSaveLocal}>Save (Local)</button> | |
| <button onClick={handleDownloadFile}>Download File</button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Run menu */} | |
| <div className="ide-menu-wrapper"> | |
| <button | |
| className="ide-menu-item" | |
| onClick={() => toggleMenu("run")} | |
| > | |
| Run ▾ | |
| </button> | |
| {openMenu === "run" && ( | |
| <div className="ide-menu-dropdown"> | |
| <button onClick={handleRun}>Run Current File</button> | |
| </div> | |
| )} | |
| </div> | |
| {/* AI menu */} | |
| <div className="ide-menu-wrapper"> | |
| <button | |
| className="ide-menu-item" | |
| onClick={() => toggleMenu("ai")} | |
| > | |
| AI ▾ | |
| </button> | |
| {openMenu === "ai" && ( | |
| <div className="ide-menu-dropdown"> | |
| <button onClick={handleAskFix}>Fix Current File</button> | |
| <button onClick={handleExplainSelection}>Explain Selection</button> | |
| </div> | |
| )} | |
| </div> | |
| <button className="ide-menu-item" onClick={() => setOutput("DevMate IDE - simple VS Code style IDE with AI agent and runner.")}> | |
| Help | |
| </button> | |
| </div> | |
| <div className="ide-menubar-right"> | |
| <span className="ide-menu-item" onClick={toggleTheme}> | |
| {theme === "vs-dark" ? "☀️ Light" : "🌙 Dark"} | |
| </span> | |
| </div> | |
| </div> | |
| {/* Main body: sidebar + editor + panel */} | |
| <div className="ide-body"> | |
| {/* Explorer Sidebar */} | |
| <div className="ide-sidebar"> | |
| <div className="ide-sidebar-header"> | |
| <span>EXPLORER</span> | |
| <button className="ide-icon-button" onClick={handleNewFile} title="New File"> | |
| + | |
| </button> | |
| </div> | |
| <div className="ide-file-list"> | |
| {Object.keys(files).map((fname) => { | |
| const fLang = files[fname].language; | |
| const fMeta = | |
| LANGUAGE_OPTIONS.find((l) => l.id === fLang) || | |
| LANGUAGE_OPTIONS[0]; | |
| return ( | |
| <div | |
| key={fname} | |
| className={ | |
| "ide-file-item" + | |
| (fname === activeFile ? " ide-file-item-active" : "") | |
| } | |
| onClick={() => setActiveFile(fname)} | |
| > | |
| <span className="ide-file-icon">{fMeta.icon}</span> | |
| <span className="ide-file-name">{fname}</span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Editor & bottom panel */} | |
| <div className="ide-main"> | |
| {/* Tabs + quick actions */} | |
| <div className="ide-tabs"> | |
| <div className="ide-tab-list"> | |
| {Object.keys(files).map((fname) => ( | |
| <div | |
| key={fname} | |
| className={ | |
| "ide-tab" + (fname === activeFile ? " ide-tab-active" : "") | |
| } | |
| onClick={() => setActiveFile(fname)} | |
| > | |
| <span>{fname}</span> | |
| {fname === activeFile && ( | |
| <button | |
| className="ide-tab-close" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleDeleteFile(); | |
| }} | |
| > | |
| × | |
| </button> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| <div className="ide-tab-actions"> | |
| <select | |
| value={currentLangId} | |
| onChange={(e) => handleChangeLanguage(e.target.value)} | |
| className="ide-select" | |
| > | |
| {LANGUAGE_OPTIONS.map((lang) => ( | |
| <option key={lang.id} value={lang.id}> | |
| {lang.label} | |
| </option> | |
| ))} | |
| </select> | |
| <button className="ide-button" onClick={handleRun}> | |
| ▶ Run | |
| </button> | |
| <button className="ide-button" onClick={handleAskFix}> | |
| 🤖 Fix with AI | |
| </button> | |
| <button className="ide-button" onClick={handleExplainSelection}> | |
| 📖 Explain Selection | |
| </button> | |
| </div> | |
| </div> | |
| {/* Editor */} | |
| <div className="ide-editor-wrapper"> | |
| <Editor | |
| height="100%" | |
| theme={theme} | |
| language={langMeta.monaco} | |
| value={currentFile?.content} | |
| onChange={updateActiveFileContent} | |
| onMount={(editor) => { | |
| editorRef.current = editor; | |
| }} | |
| options={{ | |
| minimap: { enabled: true }, | |
| fontSize: 14, | |
| scrollBeyondLastLine: false, | |
| }} | |
| /> | |
| </div> | |
| {/* Bottom Panel: Output + Agent Prompt + Explanation */} | |
| <div className="ide-bottom-panel"> | |
| <div className="ide-output-panel"> | |
| <div className="ide-panel-header">TERMINAL / OUTPUT</div> | |
| <pre className="ide-output-content">{output}</pre> | |
| </div> | |
| <div className="ide-agent-panel"> | |
| <div className="ide-panel-header">AI PROMPT</div> | |
| <textarea | |
| className="ide-agent-textarea" | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| placeholder="Ask AI to refactor, explain, or add features..." | |
| /> | |
| {explanation && ( | |
| <div className="ide-explanation"> | |
| <div className="ide-panel-header">AI EXPLANATION</div> | |
| <pre className="ide-output-content">{explanation}</pre> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Status Bar */} | |
| <div className="ide-statusbar"> | |
| <span>{activeFile}</span> | |
| <span>{langMeta.label}</span> | |
| <span>Theme: {theme === "vs-dark" ? "Dark" : "Light"}</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default App; | |