// src/App.js import { useState, useEffect, useRef } from "react"; import Editor from "@monaco-editor/react"; import XTerm from "./Terminal"; import { askAgent } from "./agent/assistant"; 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"; // API base (adjust if your Flask runs on different host/port) const API_BASE = ""; 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", "ts", "c", "cpp"]; function App() { // File tree state const [tree, setTree] = useState(loadTree()); const [activePath, setActivePath] = useState("main.py"); // Terminal/Session state const [sessionId, setSessionId] = useState(null); const [terminalLines, setTerminalLines] = useState([]); const [terminalOutputProp, setTerminalOutputProp] = useState(""); // last line to send to XTerm const [awaitingInput, setAwaitingInput] = useState(false); const [interactivePromptShown, setInteractivePromptShown] = useState(false); const [isRunning, setIsRunning] = useState(false); // AI/editor state const [prompt, setPrompt] = useState(""); const [explanation, setExplanation] = 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 [isFixing, setIsFixing] = useState(false); const [isExplaining, setIsExplaining] = useState(false); const editorRef = useRef(null); const fileInputRef = useRef(null); const pollRef = useRef(null); 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 ------------------ 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(); }; 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 appendTerminal = (text) => { if (text == null) return; const lines = String(text).split(/\r?\n/).filter(Boolean); setTerminalLines((prev) => { const next = [...prev, ...lines]; // set terminalOutputProp to last line so XTerm writes it setTerminalOutputProp(lines[lines.length - 1] || ""); return next; }); }; const clearTerminal = async () => { setTerminalLines([]); setTerminalOutputProp("\x1b[2J\x1b[H"); setAwaitingInput(false); setInteractivePromptShown(false); // stop any existing session if (sessionId) { try { await fetch(`${API_BASE}/stop`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId }), }); } catch {} setSessionId(null); } }; // Poller to /read output const startPoller = (sid) => { if (!sid) return; // clear previous if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } pollRef.current = setInterval(async () => { try { const res = await fetch(`${API_BASE}/read`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sid }), }); const data = await res.json(); if (!data) return; if (Array.isArray(data.output) && data.output.length) { data.output.forEach((ln) => appendTerminal(ln)); } if (data.finished) { // stop poller, clear sessionId if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } setSessionId(null); setAwaitingInput(false); setInteractivePromptShown(false); appendTerminal("[Process finished]"); } else { // heuristics: if last output line looks like input prompt, set awaitingInput // we'll check the terminal lines const last = terminalLines[terminalLines.length - 1] || ""; const maybePrompt = last + (data.output && data.output.length ? data.output[data.output.length - 1] : ""); const promptWords = /(enter|input|please enter|provide input|number|value|scanner|press enter)/i; if (promptWords.test(maybePrompt)) { setAwaitingInput(true); setInteractivePromptShown(true); // focus XTerm by toggling a prop (we pass autoFocusWhen = awaitingInput below) } } } catch (e) { // ignore transient errors } }, 250); }; // Start session (POST /start) // improved startSession: surfaces backend errors const startSession = async (code, filename) => { try { const res = await fetch(`${API_BASE}/start`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, filename }), }); // try parse JSON (backend returns JSON even on errors) let data = null; try { data = await res.json(); } catch (e) { // non-json response (HTML or other) โ€” surface raw text const text = await res.text(); appendTerminal(`[start] unexpected non-json response:\n${text}`); return null; } if (!res.ok || data.error) { // backend signalled an error; try to extract helpful message const msg = Array.isArray(data.output) ? data.output.join("\n") : (data.output || data.message || JSON.stringify(data)); appendTerminal(`[start] backend error:\n${msg}`); return null; } const sid = data.session_id; // initial output lines (if any) if (Array.isArray(data.output) && data.output.length) { data.output.forEach((ln) => appendTerminal(ln)); } // start polling startPoller(sid); return sid; } catch (err) { // network or unexpected error appendTerminal(`[start] fetch failed: ${String(err)}`); console.error("startSession error:", err); return null; } }; // Write input to session (POST /write) const writeToSession = async (sid, text) => { if (!sid) return; try { await fetch(`${API_BASE}/write`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sid, text }), }); } catch (e) { appendTerminal(`[Error sending input: ${e}]`); } }; // Run handler โ€” starts session for runnable files; non-runnable will show content // improved handleRun: surfaces backend errors and logs // improved handleRun: surfaces backend errors and logs const handleRun = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { appendTerminal("Select a file to run."); return; } // clear previous session + terminal for a fresh run if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } if (sessionId) { try { await fetch(`${API_BASE}/stop`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId }), }); } catch (e) { // ignore } setSessionId(null); } setTerminalLines([]); setTerminalOutputProp(""); setAwaitingInput(false); setInteractivePromptShown(false); setIsRunning(true); const filename = node.name; // start session and show backend errors (if any) const sid = await startSession(node.content || "", filename); setIsRunning(false); if (!sid) { appendTerminal("[Failed to start session] (see above server message)"); return; } setSessionId(sid); appendTerminal(`[Session started: ${sid}]`); }; // Called when XTerm or small Send triggers input const onTerminalInput = async (line) => { if (!sessionId) { appendTerminal("[No active session. Press Run first]"); return; } // echo to terminal & send appendTerminal(`> ${line}`); await writeToSession(sessionId, line); setAwaitingInput(false); setInteractivePromptShown(false); }; // clear/stop session const stopSession = async () => { if (!sessionId) return; try { await fetch(`${API_BASE}/stop`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId }), }); } catch {} if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } setSessionId(null); setAwaitingInput(false); setInteractivePromptShown(false); appendTerminal("[Session stopped]"); }; // ------------------ AI helpers (unchanged) ------------------ const handleAskFix = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { appendTerminal("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 file (${node.name}). ${userHint}\nReturn ONLY updated code, no explanation.\n\nCODE:\n${node.content}` ); const updatedTree = updateFileContent(tree, node.path, reply); setTree(updatedTree); appendTerminal("[AI] Applied fixes to file."); } catch (err) { appendTerminal(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 { let selectedCode = ""; try { selectedCode = editorRef.current?.getModel()?.getValueInRange(editorRef.current.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 fetch 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 {} }; // ------------------ 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)); }; // update file content const updateActiveFileContent = (value) => { const updated = updateFileContent(tree, activePath, value ?? ""); setTree(updated); }; 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; // UI JSX return (
โš™๏ธ DevMate IDE
{(isRunning || isFixing || isExplaining) && (
)}
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
{ // XTerm will fire onData when Enter pressed (wrapper behavior) // data may be newline or typed chunks; for safety, only trigger when newline or non-empty const trimmed = String(data || "").replace(/\r/g, ""); if (!trimmed) return; // When wrapper sends "\n" for Enter, treat it as "send what user typed in last visible line" // Simpler approach: the wrapper isn't buffering; rely on user to type line and press Send (small input), OR // handle newline by sending the last terminal input we have (not tracked here). // We'll not call runCodeWithUpdatedInput here to avoid double semantics; instead, user uses small send box below. }} /> {awaitingInput && (
This program appears to be interactive and requires console input.
Type & press Enter in terminal or use the small input below.
)} {/* Small input (optional): user can type a line and press Send to forward to backend */}
{problems.length > 0 && (
๐Ÿšจ Problems ({problems.length})
{problems.map((p, i) =>
{p.path}:{p.line} โ€” {p.message}
)}
)}
๐Ÿค– AI Assistant