// src/App.js 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"; // your existing 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 =================== // patterns that indicate program is waiting for 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, /enter a value/i, ]; return patterns.some((p) => p.test(o)); } // code-level heuristics to detect input calls function codeNeedsInput(code, langId) { if (!code) return false; try { const c = code.toString(); if (langId === "python") { 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; } 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; } if (langId === "javascript") { if (/process\.stdin|readline|readlineSync|prompt\(|require\(['"]readline['"]\)/i.test(c)) return true; } 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; } 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(); const len = ta.value?.length ?? 0; ta.setSelectionRange(len, len); } catch {} } else { const cont = document.getElementById("terminal-container"); if (cont) cont.focus(); } }, 120); } // =================== APP =================== function App() { // ----- file tree + selection ----- const [tree, setTree] = useState(loadTree()); const [activePath, setActivePath] = useState("main.py"); // ----- terminal / interactive state ----- const [accumStdin, setAccumStdin] = useState(""); // accumulated input for interactive runs const [awaitingInput, setAwaitingInput] = useState(false); const [terminalLines, setTerminalLines] = useState([]); // visible lines in terminal area const [output, setOutput] = useState(""); // "write" prop for XTerm (Terminal component picks this up) const [interactivePromptShown, setInteractivePromptShown] = 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 [isRunning, setIsRunning] = useState(false); const [isFixing, setIsFixing] = useState(false); const [isExplaining, setIsExplaining] = useState(false); // refs & helpers const editorRef = useRef(null); const fileInputRef = 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 actions ---------- 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) => { // push to visible lines and set `output` (which Terminal writes) setTerminalLines((prev) => { const next = [...prev, text]; // also keep the XTerm single-output prop to trigger Terminal.writeln setOutput(text); return next; }); }; const clearTerminal = () => { // ANSI sequence to clear screen + move cursor home (xterm will honor) setTerminalLines([]); setOutput("\x1b[2J\x1b[H"); setAccumStdin(""); setAwaitingInput(false); setInteractivePromptShown(false); }; const resetTerminal = (keepAccum = false) => { setTerminalLines([]); setOutput(""); if (!keepAccum) { setAccumStdin(""); } setAwaitingInput(false); setInteractivePromptShown(false); }; // Unified runner used when terminal provides input (or small input) const runCodeWithUpdatedInput = async (inputLine) => { if (typeof inputLine !== "string") inputLine = String(inputLine || ""); const trimmed = inputLine.replace(/\r$/, ""); // if user pressed Enter with empty line and no accum, ignore if (trimmed.length === 0 && !accumStdin) { return; } // append newline like console const newAccum = (accumStdin || "") + trimmed + "\n"; setAccumStdin(newAccum); setInteractivePromptShown(false); const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { appendTerminal("[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)) { appendTerminal(`[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 ?? ""; if (out) appendTerminal(out); setProblems(res.error ? parseProblems(res.output) : []); if (outputLooksForInput(out)) { setAwaitingInput(true); focusXtermHelper(); } else { setAwaitingInput(false); setAccumStdin(""); // finished -> clear accumulated input so next Run is fresh } } catch (err) { appendTerminal(String(err)); setAwaitingInput(true); } finally { setIsRunning(false); } }; // ---------- Initial Run handler (fresh runs) ---------- const handleRun = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { appendTerminal("Select a file to run."); return; } const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id; if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) { appendTerminal(`โ ๏ธ Run not supported for this file type.`); return; } // Force fresh run: clear accumulated input and terminal setAccumStdin(""); resetTerminal(false); setAwaitingInput(false); setInteractivePromptShown(false); const needs = codeNeedsInput(node.content, selectedLang); if (needs) { appendTerminal("[Interactive program detected โ type input directly into the terminal]"); setAwaitingInput(true); setInteractivePromptShown(true); focusXtermHelper(); return; // wait for user's input to avoid EOFError } // Non-interactive: run immediately with empty stdin appendTerminal(`[Running (fresh)]`); setIsRunning(true); setProblems([]); try { const res = await runCode(node.content, selectedLang, ""); const out = res.output ?? ""; if (out) appendTerminal(out); setProblems(res.error ? parseProblems(res.output) : []); if (outputLooksForInput(out)) { setAwaitingInput(true); focusXtermHelper(); } else { setAwaitingInput(false); setAccumStdin(""); } } catch (err) { appendTerminal(String(err)); setAwaitingInput(true); focusXtermHelper(); } finally { setIsRunning(false); } }; // ---------- Agent functions ---------- 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 ${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); 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 { 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 (