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"); // selected file or folder path 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); // {x,y,file} const editorRef = useRef(null); const fileInputRef = useRef(null); // Always persist tree on change useEffect(() => { saveTree(tree); }, [tree]); // helpers const currentNode = getNodeByPath(tree, activePath); const langMeta = LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) || LANGUAGE_OPTIONS[0]; // ---------- Tree utilities ---------- 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; }; // ---------- File / Folder actions ---------- // Create a new file inside selected folder (or root) const handleNewFile = () => { const filename = window.prompt("Filename (with extension):", "untitled.js"); if (!filename) return; // Determine default parent 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("/"); } // Offer user to choose another parent folder 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); // Set active to newly created file const newPath = (targetParent ? targetParent + "/" : "") + filename; setActivePath(newPath); }; // Create a folder under selected folder or root 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); }; // Rename selected node (file or folder) 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); // compute new activePath (preserve parent) const parts = activePath.split("/"); parts.pop(); const parent = parts.join("/"); const newPath = (parent ? parent + "/" : "") + newName; setActivePath(newPath); }; // Delete selected node (confirm if folder not empty) 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(""); // deselect / fallback }; // Download single file 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 from user's machine into selected folder const handleImportFileClick = () => fileInputRef.current?.click(); const handleFileInputChange = async (e) => { const f = e.target.files?.[0]; if (!f) return; const text = await f.text(); // decide parent folder const selected = getNodeByPath(tree, activePath); let parentPath = ""; if (selected?.type === "folder") parentPath = selected.path; // fallback // (above line intentionally split to maintain code clarity) // compute parent path correctly: if (selected?.type === "folder") parentPath = selected.path; else if (selected?.type === "file") parentPath = selected.path.split("/").slice(0, -1).join(""); // create file under parent and write content 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 = ""; }; // ---------- Run & Agent ---------- 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 res = await runCode(node.content, selectedLang, stdin); setOutput(res.output || ""); setProblems(res.error ? parseProblems(res.output) : []); }; const handleAskFix = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { setOutput("Select a file to apply fix."); return; } 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); }; const handleExplainSelection = async () => { const node = getNodeByPath(tree, activePath); if (!node || node.type !== "file") { setExplanation("Select a file to explain."); return; } 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); }; // AI suggestions for continuation (simple) 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; 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 (moved inside component so it sees activePath) ---------- const renderTree = (node, depth = 0) => { const isActive = node.path === activePath; return (
{output}
setStdin(e.target.value)}
/>
{problems.length > 0 && (