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"]; // Utility: detect whether output likely requests input function outputLooksForInput(output) { if (!output) return false; const o = output.toString(); // common prompt words / patterns and trailing prompt char const patterns = [ /enter.*:/i, /input.*:/i, /please enter/i, /scanner/i, // java Scanner exceptions or prompts /press enter/i, /: $/, // ends with colon + space (e.g. "Enter number: ") /:\n$/, // ends with colon + newline /> $/, // trailing > ]; return patterns.some((p) => p.test(o)); } // =================== APP =================== function App() { const [tree, setTree] = useState(loadTree()); const [activePath, setActivePath] = useState("main.py"); // selected file or folder path // Terminal-ish state const [terminalLines, setTerminalLines] = useState([]); // array of strings shown in terminal const [terminalInput, setTerminalInput] = useState(""); // current typed input in terminal prompt const [accumStdin, setAccumStdin] = useState(""); // accumulated stdin passed to backend const [awaitingInput, setAwaitingInput] = useState(false); // true if program waiting for input const [output, setOutput] = useState(""); // last raw output (kept for compatibility) const [prompt, setPrompt] = useState(""); const [explanation, setExplanation] = useState(""); // other states const [stdin, setStdin] = useState(""); // legacy single-run input (kept for compatibility) 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); // {x,y,file} const editorRef = useRef(null); const fileInputRef = useRef(null); // NEW: loading states const [isRunning, setIsRunning] = useState(false); const [isFixing, setIsFixing] = useState(false); const [isExplaining, setIsExplaining] = useState(false); // Always persist tree on change useEffect(() => { saveTree(tree); }, [tree]); // helpers const currentNode = getNodeByPath(tree, activePath); const langMeta = LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) || LANGUAGE_OPTIONS[0]; // ---------- TREE helpers ---------- const collectFolderPaths = (node, acc = []) => { if (!node) return acc; if (node.type === "folder") acc.push(node.path || ""); node.children?.forEach((c) => collectFolderPaths(c, acc)); return acc; }; // ---------- File / Folder actions ---------- const handleNewFile = () => { const filename = window.prompt("Filename (with extension):", "untitled.js"); if (!filename) return; // Determine default parent const selected = getNodeByPath(tree, activePath); let parentPath = ""; if (selected?.type === "folder") parentPath = selected.path; else if (selected?.type === "file") { const parts = selected.path.split("/").slice(0, -1); parentPath = parts.join("/"); } const folders = collectFolderPaths(tree); const suggestion = parentPath || folders[0] || ""; const chosen = window.prompt( `Parent folder (enter path). Available:\n${folders.join("\n")}\n\nLeave empty for root.`, suggestion ); const targetParent = chosen == null ? parentPath : (chosen.trim() || ""); const updated = addFile(tree, filename, targetParent); setTree(updated); const newPath = (targetParent ? targetParent + "/" : "") + filename; setActivePath(newPath); }; const handleNewFolder = () => { const name = window.prompt("Folder name:", "new_folder"); if (!name) return; const selected = getNodeByPath(tree, activePath); const parentPath = selected && selected.type === "folder" ? selected.path : ""; const updated = addFolder(tree, name, parentPath); setTree(updated); }; const handleRename = () => { if (!activePath) return; const node = getNodeByPath(tree, activePath); if (!node) return; const newName = window.prompt("New name:", node.name); if (!newName || newName === node.name) return; const updated = renameNode(tree, activePath, newName); setTree(updated); const parts = activePath.split("/"); parts.pop(); const parent = parts.join("/"); const newPath = (parent ? parent + "/" : "") + newName; setActivePath(newPath); }; const handleDelete = () => { if (!activePath) return; const node = getNodeByPath(tree, activePath); if (!node) return; if (node.type === "folder" && node.children?.length > 0) { const ok = window.confirm(`Folder "${node.name}" has ${node.children.length} items. Delete anyway?`); if (!ok) return; } else { const ok = window.confirm(`Delete "${node.name}"?`); if (!ok) return; } const updated = deleteNode(tree, activePath); setTree(updated); setActivePath(""); }; const downloadFile = () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") return; const blob = new Blob([node.content || ""], { type: "text/plain;charset=utf-8" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = node.name; a.click(); }; // Import file from user's machine into selected folder const handleImportFileClick = () => fileInputRef.current?.click(); const handleFileInputChange = async (e) => { const f = e.target.files?.[0]; if (!f) return; const text = await f.text(); const selected = getNodeByPath(tree, activePath); let parentPath = ""; if (selected?.type === "folder") parentPath = selected.path; else if (selected?.type === "file") parentPath = selected.path.split("/").slice(0, -1).join(""); const updated = addFile(tree, f.name, parentPath); const newPath = (parentPath ? parentPath + "/" : "") + f.name; const finalTree = updateFileContent(updated, newPath, text); setTree(finalTree); setActivePath(newPath); e.target.value = ""; }; // ---------- Terminal/Run logic (interactive) ---------- // We will accumulate stdin in `accumStdin`. On initial run we clear it. // When backend returns output containing cues that input is requested, set awaitingInput=true. // When user types into terminal prompt and presses Enter, append that input to accumStdin + "\n", // then re-run the program with updated accumStdin. Repeat until no input cues. const resetTerminal = () => { setTerminalLines([]); setTerminalInput(""); setAccumStdin(""); setAwaitingInput(false); }; const appendTerminal = (text) => { setTerminalLines((prev) => [...prev, text]); }; // helper: detect input calls in source (python/java/js) function codeNeedsInput(code, langId) { if (!code) return false; try { const c = code.toString(); if (langId === "python") { return /\binput\s*\(/i.test(c); } if (langId === "java") { return /\bScanner\s*\(|\bnext(Int|Line|Double|())\b/i.test(c) || /\bSystem\.console\(\)/i.test(c); } if (langId === "javascript") { // node.js stdin reads or prompt usage heuristics return /process\.stdin|readline|prompt\(|window\.prompt/i.test(c); } // fallback: look for common words return /\binput\b|\bScanner\b|\bnextInt\b|prompt\(/i.test(c); } catch { return false; } } const handleRun = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { setOutput("Select a file to run."); return; } const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id; if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) { setOutput(`โš ๏ธ Run not supported for this file type.`); return; } // If code likely needs input and accumStdin is empty, ask for initial input const needs = codeNeedsInput(node.content, selectedLang); if (needs && !accumStdin) { // collect multi-line input: user can paste multiple lines separated by \n const userText = window.prompt( "This program appears to request console input (e.g. input() / Scanner).\n\nEnter input lines separated by a newline (\\n). Leave empty to run without input.", "" ); if (userText == null) { // user pressed cancel: proceed but warn appendTerminal("[Run cancelled by user]"); return; } // userText may contain literal backslash-n if pasted; keep it as-is. // Normalize: if the user typed using Enter, it's already multi-line. const prepared = userText.replace(/\\n/g, "\n"); setAccumStdin(prepared); } // clear terminal state and start a fresh run resetTerminal(); setIsRunning(true); setProblems([]); setOutput(""); try { // call backend with current accumStdin (empty on first run) const res = await runCode(node.content, selectedLang, accumStdin || stdin || ""); const out = res.output ?? ""; setOutput(out); appendTerminal(out); setProblems(res.error ? parseProblems(res.output) : []); if (outputLooksForInput(out)) { // program likely wants input: show prompt setAwaitingInput(true); } else { setAwaitingInput(false); } } catch (err) { const e = String(err); setOutput(e); appendTerminal(e); setAwaitingInput(false); } finally { setIsRunning(false); } }; // Called when user types input into terminal and presses Enter const sendTerminalInput = async () => { if (!currentNode || currentNode.type !== "file") { appendTerminal("No file selected."); setAwaitingInput(false); return; } // If not already in awaitingInput (heuristic missed), still allow sending input: if (!awaitingInput && !isRunning && terminalInput.trim()) { appendTerminal("[Info] Sending input to program (re-run)."); } const userText = terminalInput; appendTerminal(`> ${userText}`); const newAccum = (accumStdin || "") + userText + "\n"; setAccumStdin(newAccum); setTerminalInput(""); setIsRunning(true); try { const selectedLang = LANGUAGE_OPTIONS.find((l) => currentNode.name.endsWith(l.ext))?.id; const res = await runCode(currentNode.content, selectedLang, newAccum); const out = res.output ?? ""; setOutput(out); appendTerminal(out); setProblems(res.error ? parseProblems(res.output) : []); if (outputLooksForInput(out)) { setAwaitingInput(true); } else { setAwaitingInput(false); } } catch (err) { appendTerminal(String(err)); setAwaitingInput(false); } finally { setIsRunning(false); } }; // Allow pressing Enter to send input const onTerminalKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); if (!awaitingInput) return; sendTerminalInput(); } }; // ---------- Agent functions ---------- const handleAskFix = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { setOutput("Select a file to apply fix."); return; } setIsFixing(true); try { const userHint = prompt.trim() ? `User request: ${prompt}` : ""; const reply = await askAgent( `Improve, debug, or refactor this ${LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id || "file"} file.\n${userHint}\nReturn ONLY updated code, no explanation.\n\nCODE:\n${node.content}` ); const updatedTree = updateFileContent(tree, node.path, reply); setTree(updatedTree); } catch (err) { setOutput(String(err)); } finally { setIsFixing(false); } }; const handleExplainSelection = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { setExplanation("Select a file to explain."); return; } setIsExplaining(true); try { const editor = editorRef.current; let selectedCode = ""; try { selectedCode = editor?.getModel()?.getValueInRange(editor.getSelection()) || ""; } catch {} const code = selectedCode.trim() || node.content; const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation."; const reply = await askAgent( `Explain what this code does, any risks, and improvements.\n${userHint}\n\nCODE:\n${code}` ); setExplanation(reply); } catch (err) { setExplanation(String(err)); } finally { setIsExplaining(false); } }; // AI suggestions for continuation (simple) const fetchAiSuggestions = async (code) => { if (!code?.trim()) return; try { const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`); setAiSuggestions(reply.split("\n").filter((l) => l.trim())); } catch { // ignore suggestion errors } }; // ---------- Search ---------- const handleSearchToggle = () => setSearchOpen(!searchOpen); const handleSearchNow = () => { if (!searchQuery) return; const results = searchTree(tree, searchQuery); alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2)); }; // ---------- Editor change ---------- const updateActiveFileContent = (value) => { const node = getNodeByPath(tree, activePath); if (!node) return; const updated = updateFileContent(tree, activePath, value ?? ""); setTree(updated); }; // ---------- Render Tree (inside component) ---------- const renderTree = (node, depth = 0) => { const isActive = node.path === activePath; return (
setActivePath(node.path)} onContextMenu={(e) => { e.preventDefault(); setActivePath(node.path); setContextMenu({ x: e.pageX, y: e.pageY, file: node.path }); }} style={{ display: "flex", alignItems: "center", gap: 8 }} > {node.type === "folder" ? "๐Ÿ“" : "๐Ÿ“„"} {node.name}
{node.children && node.children.map((c) => renderTree(c, depth + 1))}
); }; // any loading? const anyLoading = isRunning || isFixing || isExplaining; // ---------- JSX UI ---------- return (
{/* Hidden file input for import */} {/* Top menu */}
โš™๏ธ DevMate IDE
{/* Indeterminate progress bar under menubar when busy */} {anyLoading && (
)} {/* Body */}
{/* Sidebar */}
EXPLORER
{renderTree(tree)}
{/* Main (editor + bottom panels) */}
(editorRef.current = editor)} onBlur={() => fetchAiSuggestions(currentNode?.content)} options={{ minimap: { enabled: true }, fontSize: 14, scrollBeyondLastLine: false }} />
{/* AI inline suggestions popup */} {aiSuggestions.length > 0 && (
{aiSuggestions.map((s, i) => (
updateActiveFileContent((currentNode?.content || "") + "\n" + s)}>{s}
))}
)} {/* Bottom panels */}
{/* Terminal output */}
Terminal
{terminalLines.length === 0 ?
[Program output will appear here]
: terminalLines.map((ln, i) =>
{ln}
)} {awaitingInput &&
 
}
{/* Terminal input (shown only when program asks input) */} {awaitingInput ? (
setTerminalInput(e.target.value)} onKeyDown={onTerminalKeyDown} disabled={!awaitingInput || isRunning} />
) : ( // If not awaiting input, show legacy single-run input + hint to run for interactive programs
setStdin(e.target.value)} />
Press Run โ†’ to execute
)}
{/* Problems */} {problems.length > 0 && (
๐Ÿšจ Problems ({problems.length})
{problems.map((p, i) =>
{p.path}:{p.line} โ€” {p.message}
)}
)}
{/* Right AI panel */}
๐Ÿค– AI Assistant