CodeIDE / src /App.js
FrederickSundeep's picture
commit initial 09-12-2025 013
5cb8e2c
raw
history blame
11.3 kB
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"];
// =================== APP ===================
function App() {
const [tree, setTree] = useState(loadTree());
const [activePath, setActivePath] = useState("main.py");
const [output, setOutput] = useState("");
const [prompt, setPrompt] = useState("");
const [explanation, setExplanation] = 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); // right-click menu
const editorRef = useRef(null);
const currentFile = getNodeByPath(tree, activePath);
const langMeta =
LANGUAGE_OPTIONS.find((l) => currentFile?.name.endsWith(l.ext)) ||
LANGUAGE_OPTIONS[0];
// Save after tree change
useEffect(() => {
saveTree(tree);
}, [tree]);
// =================== FILE ACTIONS ===================
const handleNewFile = () => {
const name = window.prompt("Filename (with extension):");
if (!name) return;
setTree(addFile(tree, name));
};
const handleNewFolder = () => {
const name = window.prompt("Folder name:");
if (!name) return;
setTree(addFolder(tree, name));
};
const handleRename = () => {
if (!activePath) return;
const newName = window.prompt("New name:", currentFile.name);
if (!newName) return;
setTree(renameNode(tree, activePath, newName));
setActivePath(newName);
};
const handleDelete = () => {
if (!activePath) return;
setTree(deleteNode(tree, activePath));
setActivePath(""); // unselect
};
const downloadFile = () => {
if (!currentFile?.content) return;
const blob = new Blob([currentFile.content], {
type: "text/plain;charset=utf-8",
});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = currentFile.name;
a.click();
};
// =================== RUN ===================
const handleRun = async () => {
if (!currentFile?.content) return;
const selectedLang = langMeta.id;
if (!RUNNABLE_LANGS.includes(selectedLang)) {
setOutput(`⚠️ Cannot run ${selectedLang}.`);
return;
}
const res = await runCode(currentFile.content, selectedLang, stdin);
setOutput(res.output || "");
setProblems(res.error ? parseProblems(res.output) : []);
};
// =================== AI ===================
const handleAskFix = async () => {
if (!currentFile) return;
const userHint = prompt.trim() ? `User request: ${prompt}` : "";
const reply = await askAgent(
`Improve, debug, or refactor this ${langMeta.id} file.
${userHint}
Return ONLY updated code, no explanation.
CODE:
${currentFile.content}`
);
updateActiveFileContent(reply);
};
const handleExplainSelection = async () => {
if (!currentFile) return;
const editor = editorRef.current;
const selected = editor
.getModel()
.getValueInRange(editor.getSelection());
const code = selected.trim() || currentFile.content;
const userHint = prompt.trim()
? `Focus on: ${prompt}`
: "Give a clear and simple explanation.";
const reply = await askAgent(
`Explain what this ${langMeta.id} code does, any risks, and improvements.
${userHint}
CODE:
${code}`
);
setExplanation(reply);
};
const updateActiveFileContent = (value) => {
const updated = updateFileContent(tree, activePath, value);
setTree(updated);
};
// ===== AI Autocomplete Suggestions (Popup) =====
const fetchAiSuggestions = async (code) => {
if (!code?.trim()) return;
const reply = await askAgent(
`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`
);
setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
};
// =================== SEARCH ===================
const handleSearchToggle = () => setSearchOpen(!searchOpen);
const handleSearchNow = () => {
if (!searchQuery) return;
alert(
`Found occurrences:\n${JSON.stringify(
searchTree(tree, searchQuery),
null,
2
)}`
);
};
// =================== UI ===================
return (
<div
className={`ide-root ${
theme === "vs-dark" ? "ide-dark" : "ide-light"
}`}
>
{/* ==== TOP BAR ==== */}
<div className="ide-menubar">
<div className="ide-menubar-left">
<span className="ide-logo">βš™οΈ DevMate IDE</span>
<button onClick={handleNewFile}>πŸ“„ New File</button>
<button onClick={handleNewFolder}>πŸ“ New Folder</button>
<button onClick={handleRename}>✏️ Rename</button>
<button onClick={downloadFile}>πŸ“₯ Download</button>
<button onClick={downloadProjectZip}>πŸ“¦ ZIP</button>
</div>
<div className="ide-menubar-right">
<button onClick={handleSearchToggle}>πŸ” Search</button>
<button onClick={handleRun}>β–Ά Run</button>
<button onClick={handleAskFix}>πŸ€– Fix</button>
<button onClick={handleExplainSelection}>πŸ“– Explain</button>
<button
onClick={() =>
setTheme(theme === "vs-dark" ? "light" : "vs-dark")
}
>
{theme === "vs-dark" ? "β˜€οΈ" : "πŸŒ™"}
</button>
</div>
</div>
{/* ==== BODY ==== */}
<div className="ide-body">
{/* ==== TREE VIEW (LEFT) ==== */}
<div className="ide-sidebar">
{renderTree(tree, setActivePath, setContextMenu)}
</div>
{/* ==== MAIN CENTER (EDITOR + OUTPUT) ==== */}
<div className="ide-main">
<div className="ide-editor-wrapper">
<Editor
height="100%"
theme={theme}
language={langMeta.monaco}
value={currentFile?.content || ""}
onChange={updateActiveFileContent}
onMount={(editor) => (editorRef.current = editor)}
onBlur={() => fetchAiSuggestions(currentFile?.content)}
/>
</div>
{/* AI Suggestions Tooltip */}
{aiSuggestions.length > 0 && (
<div className="ai-popup">
{aiSuggestions.map((s, i) => (
<div
key={i}
className="ai-suggest"
onClick={() =>
updateActiveFileContent(
(currentFile?.content || "") + "\n" + s
)
}
>
{s}
</div>
))}
</div>
)}
{/* Bottom Panels: Output + stdin + Problems */}
<div className="ide-panels">
<pre className="ide-output">{output}</pre>
<input
className="ide-input-box"
placeholder="Program input..."
value={stdin}
onChange={(e) => setStdin(e.target.value)}
/>
{problems.length > 0 && (
<div className="ide-problems-panel">
<div>🚨 Problems ({problems.length})</div>
{problems.map((p, i) => (
<div key={i}>
{p.file}:{p.line} β€” {p.message}
</div>
))}
</div>
)}
</div>
</div>
{/* ==== RIGHT AI PANEL ==== */}
<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 (e.g., 'optimize this function', 'add comments', 'convert to JS'...)"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</div>
<div className="ide-ai-buttons">
<button onClick={handleAskFix}>πŸ’‘ Apply Fix</button>
<button onClick={handleExplainSelection}>πŸ“– 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>
{/* ==== SEARCH PANEL ==== */}
{searchOpen && (
<div className="search-dialog">
<input
placeholder="Search text..."
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button onClick={handleSearchNow}>Search</button>
</div>
)}
{/* ==== RIGHT-CLICK CONTEXT MENU ==== */}
{contextMenu && (
<div
className="ide-context-menu"
style={{ top: contextMenu.y, left: contextMenu.x }}
onMouseLeave={() => setContextMenu(null)}
>
<div onClick={handleRename}>✏️ Rename</div>
<div onClick={handleDelete}>πŸ—‘ Delete</div>
<div onClick={downloadFile}>πŸ“₯ Download</div>
</div>
)}
</div>
);
}
// =======================================================
// Tree Renderer UI
// =======================================================
function renderTree(node, setActivePath, setContextMenu, depth = 0) {
return (
<div key={node.name} style={{ paddingLeft: depth * 10 }}>
<div
className={`tree-item ${node.type}`}
onClick={() => node.type === "file" && setActivePath(node.path)}
onContextMenu={(e) => {
e.preventDefault();
setContextMenu({ x: e.pageX, y: e.pageY, file: node.path });
}}
>
{node.type === "folder" ? "πŸ“" : "πŸ“„"} {node.name}
</div>
{node.children &&
node.children.map((child) =>
renderTree(child, setActivePath, setContextMenu, depth + 1)
)}
</div>
);
}
export default App;