// App.js (complete updated file) 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"; import "xterm/css/xterm.css"; import XTerm from "./Terminal"; // keep your existing XTerm wrapper // =================== 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"]; // =================== Heuristics =================== // output patterns that indicate the program is waiting for console input function outputLooksForInput(output) { if (!output) return false; const o = output.toString(); const patterns = [ /enter.*:/i, /input.*:/i, /please enter/i, /scanner/i, /press enter/i, /: $/, /:\n$/, /> $/, /awaiting input/i, /provide input/i, /stdin/i, ]; return patterns.some((p) => p.test(o)); } // code-level heuristics to detect input() or equivalent calls function codeNeedsInput(code, langId) { if (!code) return false; try { const c = code.toString(); // Python if (langId === "python") { // input(), sys.stdin.read(), sys.stdin.readline() if (/\binput\s*\(/i.test(c)) return true; if (/\bsys\.stdin\.(read|readline|readlines)\s*\(/i.test(c)) return true; if (/\braw_input\s*\(/i.test(c)) return true; // py2 } // Java if (langId === "java") { if (/\bScanner\s*\(/i.test(c)) return true; if (/\bBufferedReader\b.*readLine/i.test(c)) return true; if (/\bSystem\.console\(\)/i.test(c)) return true; if (/\bnext(Int|Line|Double|)\b/i.test(c)) return true; } // JavaScript/Node if (langId === "javascript") { if (/process\.stdin|readline|readlineSync|prompt\(|require\(['"]readline['"]\)/i.test(c)) return true; } // C/C++/C: scanf / cin if (langId === "cpp" || langId === "c") { if (/\bscanf\s*\(/i.test(c)) return true; if (/\bstd::cin\b|cin\s*>>/i.test(c)) return true; if (/\bgets?\s*\(/i.test(c)) return true; } // Generic fallback if (/\binput\b|\bscanf\b|\bscanf_s\b|\bcin\b|\bScanner\b|readLine|readline/i.test(c)) return true; return false; } catch { return false; } } // Helper: focus xterm's hidden textarea (works with xterm.js default markup) function focusXtermHelper() { setTimeout(() => { const ta = document.querySelector("#terminal-container .xterm-helper-textarea"); if (ta) { try { ta.focus(); // move caret to end const len = ta.value?.length ?? 0; ta.setSelectionRange(len, len); } catch {} } else { // Fallback: focus container to encourage user to click const cont = document.getElementById("terminal-container"); if (cont) cont.focus(); } }, 120); // small delay to ensure xterm is ready } // =================== APP =================== function App() { const [tree, setTree] = useState(loadTree()); const [activePath, setActivePath] = useState("main.py"); // Terminal state const [accumStdin, setAccumStdin] = useState(""); const [awaitingInput, setAwaitingInput] = useState(false); const [output, setOutput] = useState(""); const [prompt, setPrompt] = useState(""); const [explanation, setExplanation] = useState(""); // legacy small-input & helpers (kept for convenience) const [terminalInput, setTerminalInput] = 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); const editorRef = useRef(null); const fileInputRef = useRef(null); // loading flags const [isRunning, setIsRunning] = useState(false); const [isFixing, setIsFixing] = useState(false); const [isExplaining, setIsExplaining] = useState(false); useEffect(() => { saveTree(tree); }, [tree]); const currentNode = getNodeByPath(tree, activePath); const langMeta = LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) || LANGUAGE_OPTIONS[0]; // ---------- File/Folder helpers (unchanged) ---------- 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; }; const handleNewFile = () => { const filename = window.prompt("Filename (with extension):", "untitled.js"); if (!filename) return; 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 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 helpers ---------- const resetTerminal = (keepAccum = false) => { setOutput(""); if (!keepAccum) { setAccumStdin(""); } setAwaitingInput(false); setTerminalInput(""); }; // Core run with updated input (used by XTerm onData and Send button) const runCodeWithUpdatedInput = async (inputLine) => { // append newline like a real console const newAccum = (accumStdin || "") + inputLine + "\n"; setAccumStdin(newAccum); const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { setOutput((prev) => prev + "\n[Error] No file selected to run."); setAwaitingInput(false); return; } const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id; if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) { setOutput((prev) => prev + `\n[Error] Run not supported for ${node.name}`); setAwaitingInput(false); return; } setIsRunning(true); try { const res = await runCode(node.content, selectedLang, newAccum); const out = res.output ?? ""; setOutput(out); setProblems(res.error ? parseProblems(res.output) : []); if (outputLooksForInput(out)) { setAwaitingInput(true); // keep focus focusXtermHelper(); } else { setAwaitingInput(false); } } catch (err) { setOutput(String(err)); setAwaitingInput(false); } finally { setIsRunning(false); } }; // Initial Run: do NOT run if code needs interactive input and there's no accumulated stdin. 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; } const needs = codeNeedsInput(node.content, selectedLang); const haveAccum = !!(accumStdin && accumStdin.length > 0); const haveLegacyStdin = !!(stdin && stdin.length > 0); if (needs && !haveAccum && !haveLegacyStdin) { // Show instruction and enable typing in XTerm; do not perform a run with empty stdin (avoids EOFError) setOutput("[Interactive program detected โ€” click Start interactive session or type in terminal]"); setAwaitingInput(true); focusXtermHelper(); return; } // Otherwise perform run with existing accumulated or legacy input const stdinToSend = accumStdin || stdin || ""; resetTerminal(true); setOutput(`[Running with stdin length=${stdinToSend ? stdinToSend.length : 0}]\n`); setIsRunning(true); setProblems([]); try { const res = await runCode(node.content, selectedLang, stdinToSend); const out = res.output ?? ""; setOutput(out); setProblems(res.error ? parseProblems(res.output) : []); if (outputLooksForInput(out)) { setAwaitingInput(true); focusXtermHelper(); } else { setAwaitingInput(false); } } catch (err) { setOutput(String(err)); // allow interactive continuation after EOF setAwaitingInput(true); focusXtermHelper(); } finally { setIsRunning(false); } }; // Send from the small input box: delegate to runCodeWithUpdatedInput to keep behavior uniform const sendTerminalInput = async () => { if (!terminalInput || terminalInput.length === 0) return; // echo in output for small box then run setOutput((prev) => prev + `\n> ${terminalInput}`); const line = terminalInput; setTerminalInput(""); await runCodeWithUpdatedInput(line); }; // handle press Enter for small input const onTerminalKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); if (!awaitingInput) return; sendTerminalInput(); } }; // ---------- Agent functions (unchanged) ---------- 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 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 } }; // ---------- 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 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))}
); }; const anyLoading = isRunning || isFixing || isExplaining; // JSX return (
โš™๏ธ DevMate IDE
{anyLoading && (
)}
EXPLORER
{renderTree(tree)}
(editorRef.current = editor)} onBlur={() => fetchAiSuggestions(currentNode?.content)} options={{ minimap: { enabled: true }, fontSize: 14, scrollBeyondLastLine: false }} />
{aiSuggestions.length > 0 && (
{aiSuggestions.map((s, i) => (
updateFileContent(tree, activePath, (currentNode?.content || "") + "\n" + s)}>{s}
))}
)}
Terminal
{ // Called when XTerm wrapper detects a full line (ENTER) runCodeWithUpdatedInput(line); }} /> {/* When interactive program detected and waiting for user input */} {awaitingInput && (
This program appears to be interactive and requires console input.
Type & press Enter in terminal or use Send below.
)} {/* If not awaiting input show legacy small input for single-run */} {!awaitingInput && (
setStdin(e.target.value)} />
Press Run โ†’ to execute
)} {/* Always show small input/send option so users can type without xterm if they prefer */}
setTerminalInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); sendTerminalInput(); } }} />
{problems.length > 0 && (
๐Ÿšจ Problems ({problems.length})
{problems.map((p, i) =>
{p.path}:{p.line} โ€” {p.message}
)}
)}
๐Ÿค– AI Assistant