CodeIDE-Dev / src /App.js
FrederickSundeep's picture
commit initial 12-12-2025 0005
53a6aba
// 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 (
<div key={node.path || node.name} style={{ paddingLeft: depth * 10 }}>
<div
className={`tree-item ${node.type} ${isActive ? "ide-file-item-active" : ""}`}
onClick={() => 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 }}
>
<span style={{ width: 18 }}>{node.type === "folder" ? "πŸ“" : "πŸ“„"}</span>
<span className="ide-file-name">{node.name}</span>
</div>
{node.children && node.children.map((c) => renderTree(c, depth + 1))}
</div>
);
};
const anyLoading = isRunning || isFixing || isExplaining;
// UI JSX
return (
<div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`} style={{ overflowX: "hidden" }}>
<input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
<div className="ide-menubar">
<div className="ide-menubar-left">
<span className="ide-logo">βš™οΈ DevMate IDE</span>
<button onClick={handleNewFile} disabled={anyLoading}>πŸ“„ New File</button>
<button onClick={handleNewFolder} disabled={anyLoading}>πŸ“ New Folder</button>
<button onClick={handleRename} disabled={anyLoading}>✏️ Rename</button>
<button onClick={handleDelete} disabled={anyLoading}>πŸ—‘ Delete</button>
<button onClick={downloadFile} disabled={anyLoading}>πŸ“₯ Download</button>
<button onClick={() => downloadProjectZip()} disabled={anyLoading}>πŸ“¦ ZIP</button>
<button onClick={handleImportFileClick} disabled={anyLoading}>πŸ“€ Import File</button>
</div>
<div className="ide-menubar-right">
<button onClick={handleSearchToggle} disabled={anyLoading}>πŸ” Search</button>
<button onClick={handleRun} disabled={isRunning || anyLoading}>{isRunning ? "⏳ Running..." : "β–Ά Run"}</button>
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>{isFixing ? "⏳ Fixing..." : "πŸ€– Fix"}</button>
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>{isExplaining ? "⏳ Explaining" : "πŸ“– Explain"}</button>
<button onClick={() => setTheme((t) => (t === "vs-dark" ? "light" : "vs-dark"))} disabled={anyLoading}>
{theme === "vs-dark" ? "β˜€οΈ" : "πŸŒ™"}
</button>
<button onClick={clearTerminal} disabled={anyLoading} title="Clear terminal">🧹 Clear</button>
<button onClick={stopSession} disabled={!sessionId}>⏹ Stop</button>
</div>
</div>
{(isRunning || isFixing || isExplaining) && (
<div className="ide-progress-wrap">
<div className="ide-progress" />
</div>
)}
<div className="ide-body">
<div className="ide-sidebar">
<div className="ide-sidebar-header">
<span>EXPLORER</span>
<button className="ide-icon-button" onClick={handleNewFile} title="New File" disabled={anyLoading}>οΌ‹</button>
</div>
<div className="ide-file-list" style={{ padding: 6 }}>{renderTree(tree)}</div>
</div>
<div className="ide-main">
<div className="ide-editor-wrapper">
<Editor
height="100%"
theme={theme}
language={langMeta.monaco}
value={currentNode?.content || ""}
onChange={updateActiveFileContent}
onMount={(editor) => (editorRef.current = editor)}
onBlur={() => fetchAiSuggestions(currentNode?.content)}
options={{ minimap: { enabled: true }, fontSize: 14, scrollBeyondLastLine: false }}
/>
</div>
{aiSuggestions.length > 0 && (
<div className="ai-popup">
{aiSuggestions.map((s, i) => (
<div key={i} className="ai-suggest" onClick={() => updateFileContent(tree, activePath, (currentNode?.content || "") + "\n" + s)}>{s}</div>
))}
</div>
)}
<div className="ide-panels">
<div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
<XTerm
output={terminalOutputProp}
autoFocusWhen={awaitingInput}
onData={(data) => {
// 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 && (
<div style={{ marginTop: 8, padding: 8, background: "#252526", border: "1px solid #333", borderRadius: 6 }}>
<div style={{ marginBottom: 6, color: "#ddd" }}>
This program appears to be interactive and requires console input.
</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => {
// focus XTerm via DOM helper
const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
if (ta) ta.focus();
}} className="ide-button">β–Ά Focus Terminal</button>
<button onClick={() => {
clearTerminal();
appendTerminal("[Interactive session started β€” type into the terminal]");
setAwaitingInput(true);
const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
if (ta) ta.focus();
}} className="ide-button">β–Ά Start interactive session</button>
<div style={{ color: "#999", alignSelf: "center" }}>Type & press Enter in terminal or use the small input below.</div>
</div>
</div>
)}
{/* Small input (optional): user can type a line and press Send to forward to backend */}
<TerminalSend onSend={onTerminalInput} disabled={!sessionId && !awaitingInput} isRunning={isRunning} />
</div>
{problems.length > 0 && (
<div className="ide-problems-panel">
<div>🚨 Problems ({problems.length})</div>
{problems.map((p, i) => <div key={i}>{p.path}:{p.line} β€” {p.message}</div>)}
</div>
)}
</div>
</div>
<div className="ide-right-panel">
<div className="ide-ai-header">πŸ€– AI Assistant</div>
<div className="ide-ai-section">
<label className="ide-ai-label">Instruction</label>
<textarea className="ide-agent-textarea" placeholder="Ask the AI (optimize, add tests, convert, etc.)" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
</div>
<div className="ide-ai-buttons">
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>{isFixing ? "⏳ Apply Fix" : "πŸ’‘ Apply Fix"}</button>
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>{isExplaining ? "⏳ Explaining" : "πŸ“– Explain Code"}</button>
</div>
{explanation && (
<div className="ide-ai-section">
<label className="ide-ai-label">Explanation</label>
<div className="ide-explain">{explanation}</div>
</div>
)}
</div>
</div>
{searchOpen && (
<div className="search-dialog">
<input placeholder="Search text..." onChange={(e) => setSearchQuery(e.target.value)} />
<button onClick={handleSearchNow}>Search</button>
</div>
)}
{contextMenu && (
<div className="ide-context-menu" style={{ top: contextMenu.y, left: contextMenu.x }} onMouseLeave={() => setContextMenu(null)}>
<div onClick={() => { setContextMenu(null); handleRename(); }}>✏️ Rename</div>
<div onClick={() => { setContextMenu(null); handleDelete(); }}>πŸ—‘ Delete</div>
<div onClick={() => { setContextMenu(null); downloadFile(); }}>πŸ“₯ Download</div>
</div>
)}
</div>
);
}
// Small send component (kept in same file to avoid extra imports)
function TerminalSend({ onSend, disabled, isRunning }) {
const [val, setVal] = useState("");
return (
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<input
className="ide-input-box"
placeholder="Type input and press Send (or press Enter in terminal)"
value={val}
onChange={(e) => setVal(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (!val) return;
onSend(val);
setVal("");
}
}}
/>
<button onClick={() => { if (val) { onSend(val); setVal(""); } }} className="ide-button" disabled={disabled || isRunning}>
{isRunning ? "⏳" : "Send"}
</button>
</div>
);
}
export default App;