Spaces:
Running
Running
| // 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; | |