Spaces:
Running
Running
Commit Β·
eb69de5
1
Parent(s): 5b4b486
commit initial 09-12-2025 017
Browse files- src/App.css +43 -0
- src/App.js +102 -120
src/App.css
CHANGED
|
@@ -406,3 +406,46 @@
|
|
| 406 |
width: auto;
|
| 407 |
}
|
| 408 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
width: auto;
|
| 407 |
}
|
| 408 |
}
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
/* ---------- progress bar under menubar ---------- */
|
| 412 |
+
.ide-progress-wrap {
|
| 413 |
+
height: 3px;
|
| 414 |
+
background: transparent;
|
| 415 |
+
width: 100%;
|
| 416 |
+
position: relative;
|
| 417 |
+
overflow: hidden;
|
| 418 |
+
}
|
| 419 |
+
.ide-progress {
|
| 420 |
+
position: absolute;
|
| 421 |
+
height: 3px;
|
| 422 |
+
width: 30%;
|
| 423 |
+
left: -30%;
|
| 424 |
+
top: 0;
|
| 425 |
+
background: linear-gradient(90deg, #0e9, #08f);
|
| 426 |
+
animation: progress-slide 1.2s linear infinite;
|
| 427 |
+
border-radius: 2px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
@keyframes progress-slide {
|
| 431 |
+
0% { left: -30%; width: 30%; }
|
| 432 |
+
50% { left: 35%; width: 40%; }
|
| 433 |
+
100% { left: 100%; width: 30%; }
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
/* ---------- small inline spinner (optional) ---------- */
|
| 437 |
+
.button-spinner {
|
| 438 |
+
display: inline-block;
|
| 439 |
+
width: 12px;
|
| 440 |
+
height: 12px;
|
| 441 |
+
border: 2px solid rgba(255,255,255,0.25);
|
| 442 |
+
border-top-color: rgba(255,255,255,0.95);
|
| 443 |
+
border-radius: 50%;
|
| 444 |
+
animation: spin 0.8s linear infinite;
|
| 445 |
+
margin-left: 6px;
|
| 446 |
+
vertical-align: middle;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
@keyframes spin {
|
| 450 |
+
to { transform: rotate(360deg); }
|
| 451 |
+
}
|
src/App.js
CHANGED
|
@@ -49,6 +49,11 @@ function App() {
|
|
| 49 |
const editorRef = useRef(null);
|
| 50 |
const fileInputRef = useRef(null);
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
// Always persist tree on change
|
| 53 |
useEffect(() => {
|
| 54 |
saveTree(tree);
|
|
@@ -69,8 +74,6 @@ function App() {
|
|
| 69 |
};
|
| 70 |
|
| 71 |
// ---------- File / Folder actions ----------
|
| 72 |
-
|
| 73 |
-
// Create a new file inside selected folder (or root)
|
| 74 |
const handleNewFile = () => {
|
| 75 |
const filename = window.prompt("Filename (with extension):", "untitled.js");
|
| 76 |
if (!filename) return;
|
|
@@ -84,7 +87,6 @@ function App() {
|
|
| 84 |
parentPath = parts.join("/");
|
| 85 |
}
|
| 86 |
|
| 87 |
-
// Offer user to choose another parent folder
|
| 88 |
const folders = collectFolderPaths(tree);
|
| 89 |
const suggestion = parentPath || folders[0] || "";
|
| 90 |
const chosen = window.prompt(
|
|
@@ -96,12 +98,10 @@ function App() {
|
|
| 96 |
const updated = addFile(tree, filename, targetParent);
|
| 97 |
setTree(updated);
|
| 98 |
|
| 99 |
-
// Set active to newly created file
|
| 100 |
const newPath = (targetParent ? targetParent + "/" : "") + filename;
|
| 101 |
setActivePath(newPath);
|
| 102 |
};
|
| 103 |
|
| 104 |
-
// Create a folder under selected folder or root
|
| 105 |
const handleNewFolder = () => {
|
| 106 |
const name = window.prompt("Folder name:", "new_folder");
|
| 107 |
if (!name) return;
|
|
@@ -111,7 +111,6 @@ function App() {
|
|
| 111 |
setTree(updated);
|
| 112 |
};
|
| 113 |
|
| 114 |
-
// Rename selected node (file or folder)
|
| 115 |
const handleRename = () => {
|
| 116 |
if (!activePath) return;
|
| 117 |
const node = getNodeByPath(tree, activePath);
|
|
@@ -121,7 +120,6 @@ function App() {
|
|
| 121 |
const updated = renameNode(tree, activePath, newName);
|
| 122 |
setTree(updated);
|
| 123 |
|
| 124 |
-
// compute new activePath (preserve parent)
|
| 125 |
const parts = activePath.split("/");
|
| 126 |
parts.pop();
|
| 127 |
const parent = parts.join("/");
|
|
@@ -129,7 +127,6 @@ function App() {
|
|
| 129 |
setActivePath(newPath);
|
| 130 |
};
|
| 131 |
|
| 132 |
-
// Delete selected node (confirm if folder not empty)
|
| 133 |
const handleDelete = () => {
|
| 134 |
if (!activePath) return;
|
| 135 |
const node = getNodeByPath(tree, activePath);
|
|
@@ -145,10 +142,9 @@ function App() {
|
|
| 145 |
|
| 146 |
const updated = deleteNode(tree, activePath);
|
| 147 |
setTree(updated);
|
| 148 |
-
setActivePath("");
|
| 149 |
};
|
| 150 |
|
| 151 |
-
// Download single file
|
| 152 |
const downloadFile = () => {
|
| 153 |
const node = getNodeByPath(tree, activePath);
|
| 154 |
if (!node || node.type !== "file") return;
|
|
@@ -167,16 +163,11 @@ function App() {
|
|
| 167 |
if (!f) return;
|
| 168 |
const text = await f.text();
|
| 169 |
|
| 170 |
-
// decide parent folder
|
| 171 |
const selected = getNodeByPath(tree, activePath);
|
| 172 |
let parentPath = "";
|
| 173 |
-
if (selected?.type === "folder") parentPath = selected.path; // fallback
|
| 174 |
-
// (above line intentionally split to maintain code clarity)
|
| 175 |
-
// compute parent path correctly:
|
| 176 |
if (selected?.type === "folder") parentPath = selected.path;
|
| 177 |
else if (selected?.type === "file") parentPath = selected.path.split("/").slice(0, -1).join("");
|
| 178 |
|
| 179 |
-
// create file under parent and write content
|
| 180 |
const updated = addFile(tree, f.name, parentPath);
|
| 181 |
const newPath = (parentPath ? parentPath + "/" : "") + f.name;
|
| 182 |
const finalTree = updateFileContent(updated, newPath, text);
|
|
@@ -185,8 +176,7 @@ function App() {
|
|
| 185 |
e.target.value = "";
|
| 186 |
};
|
| 187 |
|
| 188 |
-
// ---------- Run & Agent ----------
|
| 189 |
-
|
| 190 |
const handleRun = async () => {
|
| 191 |
const node = getNodeByPath(tree, activePath);
|
| 192 |
if (!node || node.type !== "file") {
|
|
@@ -199,9 +189,18 @@ function App() {
|
|
| 199 |
return;
|
| 200 |
}
|
| 201 |
|
| 202 |
-
|
| 203 |
-
setOutput(
|
| 204 |
-
setProblems(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
};
|
| 206 |
|
| 207 |
const handleAskFix = async () => {
|
|
@@ -210,12 +209,19 @@ function App() {
|
|
| 210 |
setOutput("Select a file to apply fix.");
|
| 211 |
return;
|
| 212 |
}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
};
|
| 220 |
|
| 221 |
const handleExplainSelection = async () => {
|
|
@@ -224,24 +230,35 @@ function App() {
|
|
| 224 |
setExplanation("Select a file to explain.");
|
| 225 |
return;
|
| 226 |
}
|
| 227 |
-
|
| 228 |
-
let selectedCode = "";
|
| 229 |
try {
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
};
|
| 239 |
|
| 240 |
// AI suggestions for continuation (simple)
|
| 241 |
const fetchAiSuggestions = async (code) => {
|
| 242 |
if (!code?.trim()) return;
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
};
|
| 246 |
|
| 247 |
// ---------- Search ----------
|
|
@@ -261,7 +278,7 @@ function App() {
|
|
| 261 |
setTree(updated);
|
| 262 |
};
|
| 263 |
|
| 264 |
-
// ---------- Render Tree (
|
| 265 |
const renderTree = (node, depth = 0) => {
|
| 266 |
const isActive = node.path === activePath;
|
| 267 |
return (
|
|
@@ -280,60 +297,71 @@ function App() {
|
|
| 280 |
<span className="ide-file-name">{node.name}</span>
|
| 281 |
</div>
|
| 282 |
|
| 283 |
-
{node.children &&
|
| 284 |
-
node.children.map((c) => renderTree(c, depth + 1))}
|
| 285 |
</div>
|
| 286 |
);
|
| 287 |
};
|
| 288 |
|
|
|
|
|
|
|
|
|
|
| 289 |
// ---------- JSX UI ----------
|
| 290 |
return (
|
| 291 |
<div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}>
|
| 292 |
{/* Hidden file input for import */}
|
| 293 |
-
<input
|
| 294 |
-
ref={fileInputRef}
|
| 295 |
-
id="file-import-input"
|
| 296 |
-
type="file"
|
| 297 |
-
style={{ display: "none" }}
|
| 298 |
-
onChange={handleFileInputChange}
|
| 299 |
-
/>
|
| 300 |
|
| 301 |
{/* Top menu */}
|
| 302 |
<div className="ide-menubar">
|
| 303 |
<div className="ide-menubar-left">
|
| 304 |
<span className="ide-logo">βοΈ DevMate IDE</span>
|
| 305 |
|
| 306 |
-
<button onClick={handleNewFile}>π New File</button>
|
| 307 |
-
<button onClick={handleNewFolder}>π New Folder</button>
|
| 308 |
-
<button onClick={handleRename}>βοΈ Rename</button>
|
| 309 |
-
<button onClick={handleDelete}>π Delete</button>
|
| 310 |
-
<button onClick={downloadFile}>π₯ Download</button>
|
| 311 |
-
<button onClick={() => downloadProjectZip()}>π¦ ZIP</button>
|
| 312 |
-
<button onClick={handleImportFileClick}>π€ Import File</button>
|
| 313 |
</div>
|
| 314 |
|
| 315 |
<div className="ide-menubar-right">
|
| 316 |
-
<button onClick={handleSearchToggle}>π Search</button>
|
| 317 |
-
|
| 318 |
-
<button onClick={
|
| 319 |
-
|
| 320 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
{theme === "vs-dark" ? "βοΈ" : "π"}
|
| 322 |
</button>
|
| 323 |
</div>
|
| 324 |
</div>
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
{/* Body */}
|
| 327 |
<div className="ide-body">
|
| 328 |
{/* Sidebar */}
|
| 329 |
<div className="ide-sidebar">
|
| 330 |
<div className="ide-sidebar-header">
|
| 331 |
<span>EXPLORER</span>
|
| 332 |
-
<button className="ide-icon-button" onClick={handleNewFile} title="New File">οΌ</button>
|
| 333 |
-
</div>
|
| 334 |
-
<div className="ide-file-list" style={{ padding: 6 }}>
|
| 335 |
-
{renderTree(tree)}
|
| 336 |
</div>
|
|
|
|
| 337 |
</div>
|
| 338 |
|
| 339 |
{/* Main (editor + bottom panels) */}
|
|
@@ -359,15 +387,7 @@ function App() {
|
|
| 359 |
{aiSuggestions.length > 0 && (
|
| 360 |
<div className="ai-popup">
|
| 361 |
{aiSuggestions.map((s, i) => (
|
| 362 |
-
<div
|
| 363 |
-
key={i}
|
| 364 |
-
className="ai-suggest"
|
| 365 |
-
onClick={() =>
|
| 366 |
-
updateActiveFileContent((currentNode?.content || "") + "\n" + s)
|
| 367 |
-
}
|
| 368 |
-
>
|
| 369 |
-
{s}
|
| 370 |
-
</div>
|
| 371 |
))}
|
| 372 |
</div>
|
| 373 |
)}
|
|
@@ -376,12 +396,7 @@ function App() {
|
|
| 376 |
<div className="ide-panels">
|
| 377 |
<pre className="ide-output">{output}</pre>
|
| 378 |
|
| 379 |
-
<input
|
| 380 |
-
className="ide-input-box"
|
| 381 |
-
placeholder="Program input..."
|
| 382 |
-
value={stdin}
|
| 383 |
-
onChange={(e) => setStdin(e.target.value)}
|
| 384 |
-
/>
|
| 385 |
|
| 386 |
{problems.length > 0 && (
|
| 387 |
<div className="ide-problems-panel">
|
|
@@ -402,17 +417,12 @@ function App() {
|
|
| 402 |
|
| 403 |
<div className="ide-ai-section">
|
| 404 |
<label className="ide-ai-label">Instruction</label>
|
| 405 |
-
<textarea
|
| 406 |
-
className="ide-agent-textarea"
|
| 407 |
-
placeholder="Ask the AI (optimize, add tests, convert, etc.)"
|
| 408 |
-
value={prompt}
|
| 409 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 410 |
-
/>
|
| 411 |
</div>
|
| 412 |
|
| 413 |
<div className="ide-ai-buttons">
|
| 414 |
-
<button onClick={handleAskFix}>π‘ Apply Fix</button>
|
| 415 |
-
<button onClick={handleExplainSelection}>π Explain Code</button>
|
| 416 |
</div>
|
| 417 |
|
| 418 |
{explanation && (
|
|
@@ -427,45 +437,17 @@ function App() {
|
|
| 427 |
{/* Search dialog */}
|
| 428 |
{searchOpen && (
|
| 429 |
<div className="search-dialog">
|
| 430 |
-
<input
|
| 431 |
-
placeholder="Search text..."
|
| 432 |
-
onChange={(e) => setSearchQuery(e.target.value)}
|
| 433 |
-
/>
|
| 434 |
<button onClick={handleSearchNow}>Search</button>
|
| 435 |
</div>
|
| 436 |
)}
|
| 437 |
|
| 438 |
{/* Context menu */}
|
| 439 |
{contextMenu && (
|
| 440 |
-
<div
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
>
|
| 445 |
-
<div
|
| 446 |
-
onClick={() => {
|
| 447 |
-
setContextMenu(null);
|
| 448 |
-
handleRename();
|
| 449 |
-
}}
|
| 450 |
-
>
|
| 451 |
-
βοΈ Rename
|
| 452 |
-
</div>
|
| 453 |
-
<div
|
| 454 |
-
onClick={() => {
|
| 455 |
-
setContextMenu(null);
|
| 456 |
-
handleDelete();
|
| 457 |
-
}}
|
| 458 |
-
>
|
| 459 |
-
π Delete
|
| 460 |
-
</div>
|
| 461 |
-
<div
|
| 462 |
-
onClick={() => {
|
| 463 |
-
setContextMenu(null);
|
| 464 |
-
downloadFile();
|
| 465 |
-
}}
|
| 466 |
-
>
|
| 467 |
-
π₯ Download
|
| 468 |
-
</div>
|
| 469 |
</div>
|
| 470 |
)}
|
| 471 |
</div>
|
|
|
|
| 49 |
const editorRef = useRef(null);
|
| 50 |
const fileInputRef = useRef(null);
|
| 51 |
|
| 52 |
+
// NEW: loading states
|
| 53 |
+
const [isRunning, setIsRunning] = useState(false);
|
| 54 |
+
const [isFixing, setIsFixing] = useState(false);
|
| 55 |
+
const [isExplaining, setIsExplaining] = useState(false);
|
| 56 |
+
|
| 57 |
// Always persist tree on change
|
| 58 |
useEffect(() => {
|
| 59 |
saveTree(tree);
|
|
|
|
| 74 |
};
|
| 75 |
|
| 76 |
// ---------- File / Folder actions ----------
|
|
|
|
|
|
|
| 77 |
const handleNewFile = () => {
|
| 78 |
const filename = window.prompt("Filename (with extension):", "untitled.js");
|
| 79 |
if (!filename) return;
|
|
|
|
| 87 |
parentPath = parts.join("/");
|
| 88 |
}
|
| 89 |
|
|
|
|
| 90 |
const folders = collectFolderPaths(tree);
|
| 91 |
const suggestion = parentPath || folders[0] || "";
|
| 92 |
const chosen = window.prompt(
|
|
|
|
| 98 |
const updated = addFile(tree, filename, targetParent);
|
| 99 |
setTree(updated);
|
| 100 |
|
|
|
|
| 101 |
const newPath = (targetParent ? targetParent + "/" : "") + filename;
|
| 102 |
setActivePath(newPath);
|
| 103 |
};
|
| 104 |
|
|
|
|
| 105 |
const handleNewFolder = () => {
|
| 106 |
const name = window.prompt("Folder name:", "new_folder");
|
| 107 |
if (!name) return;
|
|
|
|
| 111 |
setTree(updated);
|
| 112 |
};
|
| 113 |
|
|
|
|
| 114 |
const handleRename = () => {
|
| 115 |
if (!activePath) return;
|
| 116 |
const node = getNodeByPath(tree, activePath);
|
|
|
|
| 120 |
const updated = renameNode(tree, activePath, newName);
|
| 121 |
setTree(updated);
|
| 122 |
|
|
|
|
| 123 |
const parts = activePath.split("/");
|
| 124 |
parts.pop();
|
| 125 |
const parent = parts.join("/");
|
|
|
|
| 127 |
setActivePath(newPath);
|
| 128 |
};
|
| 129 |
|
|
|
|
| 130 |
const handleDelete = () => {
|
| 131 |
if (!activePath) return;
|
| 132 |
const node = getNodeByPath(tree, activePath);
|
|
|
|
| 142 |
|
| 143 |
const updated = deleteNode(tree, activePath);
|
| 144 |
setTree(updated);
|
| 145 |
+
setActivePath("");
|
| 146 |
};
|
| 147 |
|
|
|
|
| 148 |
const downloadFile = () => {
|
| 149 |
const node = getNodeByPath(tree, activePath);
|
| 150 |
if (!node || node.type !== "file") return;
|
|
|
|
| 163 |
if (!f) return;
|
| 164 |
const text = await f.text();
|
| 165 |
|
|
|
|
| 166 |
const selected = getNodeByPath(tree, activePath);
|
| 167 |
let parentPath = "";
|
|
|
|
|
|
|
|
|
|
| 168 |
if (selected?.type === "folder") parentPath = selected.path;
|
| 169 |
else if (selected?.type === "file") parentPath = selected.path.split("/").slice(0, -1).join("");
|
| 170 |
|
|
|
|
| 171 |
const updated = addFile(tree, f.name, parentPath);
|
| 172 |
const newPath = (parentPath ? parentPath + "/" : "") + f.name;
|
| 173 |
const finalTree = updateFileContent(updated, newPath, text);
|
|
|
|
| 176 |
e.target.value = "";
|
| 177 |
};
|
| 178 |
|
| 179 |
+
// ---------- Run & Agent (with progress flags) ----------
|
|
|
|
| 180 |
const handleRun = async () => {
|
| 181 |
const node = getNodeByPath(tree, activePath);
|
| 182 |
if (!node || node.type !== "file") {
|
|
|
|
| 189 |
return;
|
| 190 |
}
|
| 191 |
|
| 192 |
+
setIsRunning(true);
|
| 193 |
+
setOutput(""); // clear previous
|
| 194 |
+
setProblems([]);
|
| 195 |
+
try {
|
| 196 |
+
const res = await runCode(node.content, selectedLang, stdin);
|
| 197 |
+
setOutput(res.output || "");
|
| 198 |
+
setProblems(res.error ? parseProblems(res.output) : []);
|
| 199 |
+
} catch (err) {
|
| 200 |
+
setOutput(String(err));
|
| 201 |
+
} finally {
|
| 202 |
+
setIsRunning(false);
|
| 203 |
+
}
|
| 204 |
};
|
| 205 |
|
| 206 |
const handleAskFix = async () => {
|
|
|
|
| 209 |
setOutput("Select a file to apply fix.");
|
| 210 |
return;
|
| 211 |
}
|
| 212 |
+
setIsFixing(true);
|
| 213 |
+
try {
|
| 214 |
+
const userHint = prompt.trim() ? `User request: ${prompt}` : "";
|
| 215 |
+
const reply = await askAgent(
|
| 216 |
+
`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}`
|
| 217 |
+
);
|
| 218 |
+
const updatedTree = updateFileContent(tree, node.path, reply);
|
| 219 |
+
setTree(updatedTree);
|
| 220 |
+
} catch (err) {
|
| 221 |
+
setOutput(String(err));
|
| 222 |
+
} finally {
|
| 223 |
+
setIsFixing(false);
|
| 224 |
+
}
|
| 225 |
};
|
| 226 |
|
| 227 |
const handleExplainSelection = async () => {
|
|
|
|
| 230 |
setExplanation("Select a file to explain.");
|
| 231 |
return;
|
| 232 |
}
|
| 233 |
+
setIsExplaining(true);
|
|
|
|
| 234 |
try {
|
| 235 |
+
const editor = editorRef.current;
|
| 236 |
+
let selectedCode = "";
|
| 237 |
+
try {
|
| 238 |
+
selectedCode = editor?.getModel()?.getValueInRange(editor.getSelection()) || "";
|
| 239 |
+
} catch {}
|
| 240 |
+
const code = selectedCode.trim() || node.content;
|
| 241 |
+
const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation.";
|
| 242 |
+
const reply = await askAgent(
|
| 243 |
+
`Explain what this code does, any risks, and improvements.\n${userHint}\n\nCODE:\n${code}`
|
| 244 |
+
);
|
| 245 |
+
setExplanation(reply);
|
| 246 |
+
} catch (err) {
|
| 247 |
+
setExplanation(String(err));
|
| 248 |
+
} finally {
|
| 249 |
+
setIsExplaining(false);
|
| 250 |
+
}
|
| 251 |
};
|
| 252 |
|
| 253 |
// AI suggestions for continuation (simple)
|
| 254 |
const fetchAiSuggestions = async (code) => {
|
| 255 |
if (!code?.trim()) return;
|
| 256 |
+
try {
|
| 257 |
+
const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
|
| 258 |
+
setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
|
| 259 |
+
} catch {
|
| 260 |
+
// ignore suggestion errors
|
| 261 |
+
}
|
| 262 |
};
|
| 263 |
|
| 264 |
// ---------- Search ----------
|
|
|
|
| 278 |
setTree(updated);
|
| 279 |
};
|
| 280 |
|
| 281 |
+
// ---------- Render Tree (inside component) ----------
|
| 282 |
const renderTree = (node, depth = 0) => {
|
| 283 |
const isActive = node.path === activePath;
|
| 284 |
return (
|
|
|
|
| 297 |
<span className="ide-file-name">{node.name}</span>
|
| 298 |
</div>
|
| 299 |
|
| 300 |
+
{node.children && node.children.map((c) => renderTree(c, depth + 1))}
|
|
|
|
| 301 |
</div>
|
| 302 |
);
|
| 303 |
};
|
| 304 |
|
| 305 |
+
// any loading?
|
| 306 |
+
const anyLoading = isRunning || isFixing || isExplaining;
|
| 307 |
+
|
| 308 |
// ---------- JSX UI ----------
|
| 309 |
return (
|
| 310 |
<div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}>
|
| 311 |
{/* Hidden file input for import */}
|
| 312 |
+
<input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
{/* Top menu */}
|
| 315 |
<div className="ide-menubar">
|
| 316 |
<div className="ide-menubar-left">
|
| 317 |
<span className="ide-logo">βοΈ DevMate IDE</span>
|
| 318 |
|
| 319 |
+
<button onClick={handleNewFile} disabled={anyLoading}>π New File</button>
|
| 320 |
+
<button onClick={handleNewFolder} disabled={anyLoading}>π New Folder</button>
|
| 321 |
+
<button onClick={handleRename} disabled={anyLoading}>βοΈ Rename</button>
|
| 322 |
+
<button onClick={handleDelete} disabled={anyLoading}>π Delete</button>
|
| 323 |
+
<button onClick={downloadFile} disabled={anyLoading}>π₯ Download</button>
|
| 324 |
+
<button onClick={() => downloadProjectZip()} disabled={anyLoading}>π¦ ZIP</button>
|
| 325 |
+
<button onClick={handleImportFileClick} disabled={anyLoading}>π€ Import File</button>
|
| 326 |
</div>
|
| 327 |
|
| 328 |
<div className="ide-menubar-right">
|
| 329 |
+
<button onClick={handleSearchToggle} disabled={anyLoading}>π Search</button>
|
| 330 |
+
|
| 331 |
+
<button onClick={handleRun} disabled={isRunning || anyLoading}>
|
| 332 |
+
{isRunning ? "β³ Running..." : "βΆ Run"}
|
| 333 |
+
</button>
|
| 334 |
+
|
| 335 |
+
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>
|
| 336 |
+
{isFixing ? "β³ Fixing..." : "π€ Fix"}
|
| 337 |
+
</button>
|
| 338 |
+
|
| 339 |
+
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>
|
| 340 |
+
{isExplaining ? "β³ Explaining..." : "π Explain"}
|
| 341 |
+
</button>
|
| 342 |
+
|
| 343 |
+
<button onClick={() => setTheme((t) => (t === "vs-dark" ? "light" : "vs-dark"))} disabled={anyLoading}>
|
| 344 |
{theme === "vs-dark" ? "βοΈ" : "π"}
|
| 345 |
</button>
|
| 346 |
</div>
|
| 347 |
</div>
|
| 348 |
|
| 349 |
+
{/* Indeterminate progress bar under menubar when busy */}
|
| 350 |
+
{anyLoading && (
|
| 351 |
+
<div className="ide-progress-wrap">
|
| 352 |
+
<div className="ide-progress" />
|
| 353 |
+
</div>
|
| 354 |
+
)}
|
| 355 |
+
|
| 356 |
{/* Body */}
|
| 357 |
<div className="ide-body">
|
| 358 |
{/* Sidebar */}
|
| 359 |
<div className="ide-sidebar">
|
| 360 |
<div className="ide-sidebar-header">
|
| 361 |
<span>EXPLORER</span>
|
| 362 |
+
<button className="ide-icon-button" onClick={handleNewFile} title="New File" disabled={anyLoading}>οΌ</button>
|
|
|
|
|
|
|
|
|
|
| 363 |
</div>
|
| 364 |
+
<div className="ide-file-list" style={{ padding: 6 }}>{renderTree(tree)}</div>
|
| 365 |
</div>
|
| 366 |
|
| 367 |
{/* Main (editor + bottom panels) */}
|
|
|
|
| 387 |
{aiSuggestions.length > 0 && (
|
| 388 |
<div className="ai-popup">
|
| 389 |
{aiSuggestions.map((s, i) => (
|
| 390 |
+
<div key={i} className="ai-suggest" onClick={() => updateActiveFileContent((currentNode?.content || "") + "\n" + s)}>{s}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
))}
|
| 392 |
</div>
|
| 393 |
)}
|
|
|
|
| 396 |
<div className="ide-panels">
|
| 397 |
<pre className="ide-output">{output}</pre>
|
| 398 |
|
| 399 |
+
<input className="ide-input-box" placeholder="Program input..." value={stdin} onChange={(e) => setStdin(e.target.value)} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
{problems.length > 0 && (
|
| 402 |
<div className="ide-problems-panel">
|
|
|
|
| 417 |
|
| 418 |
<div className="ide-ai-section">
|
| 419 |
<label className="ide-ai-label">Instruction</label>
|
| 420 |
+
<textarea className="ide-agent-textarea" placeholder="Ask the AI (optimize, add tests, convert, etc.)" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
</div>
|
| 422 |
|
| 423 |
<div className="ide-ai-buttons">
|
| 424 |
+
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>{isFixing ? "β³ Apply Fix" : "π‘ Apply Fix"}</button>
|
| 425 |
+
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>{isExplaining ? "β³ Explaining" : "π Explain Code"}</button>
|
| 426 |
</div>
|
| 427 |
|
| 428 |
{explanation && (
|
|
|
|
| 437 |
{/* Search dialog */}
|
| 438 |
{searchOpen && (
|
| 439 |
<div className="search-dialog">
|
| 440 |
+
<input placeholder="Search text..." onChange={(e) => setSearchQuery(e.target.value)} />
|
|
|
|
|
|
|
|
|
|
| 441 |
<button onClick={handleSearchNow}>Search</button>
|
| 442 |
</div>
|
| 443 |
)}
|
| 444 |
|
| 445 |
{/* Context menu */}
|
| 446 |
{contextMenu && (
|
| 447 |
+
<div className="ide-context-menu" style={{ top: contextMenu.y, left: contextMenu.x }} onMouseLeave={() => setContextMenu(null)}>
|
| 448 |
+
<div onClick={() => { setContextMenu(null); handleRename(); }}>βοΈ Rename</div>
|
| 449 |
+
<div onClick={() => { setContextMenu(null); handleDelete(); }}>π Delete</div>
|
| 450 |
+
<div onClick={() => { setContextMenu(null); downloadFile(); }}>π₯ Download</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
</div>
|
| 452 |
)}
|
| 453 |
</div>
|