FrederickSundeep commited on
Commit
5b4b486
·
1 Parent(s): 4d6c4c1

commit initial 09-12-2025 016

Browse files
Files changed (2) hide show
  1. src/App.js +247 -128
  2. src/fileStore.js +158 -72
src/App.js CHANGED
@@ -35,7 +35,7 @@ const RUNNABLE_LANGS = ["python", "javascript", "java"];
35
  // =================== APP ===================
36
  function App() {
37
  const [tree, setTree] = useState(loadTree());
38
- const [activePath, setActivePath] = useState("main.py");
39
  const [output, setOutput] = useState("");
40
  const [prompt, setPrompt] = useState("");
41
  const [explanation, setExplanation] = useState("");
@@ -45,145 +45,260 @@ function App() {
45
  const [searchOpen, setSearchOpen] = useState(false);
46
  const [searchQuery, setSearchQuery] = useState("");
47
  const [aiSuggestions, setAiSuggestions] = useState([]);
48
- const [contextMenu, setContextMenu] = useState(null); // right-click menu
49
  const editorRef = useRef(null);
 
50
 
51
- const currentFile = getNodeByPath(tree, activePath);
52
- const langMeta =
53
- LANGUAGE_OPTIONS.find((l) => currentFile?.name.endsWith(l.ext)) ||
54
- LANGUAGE_OPTIONS[0];
55
-
56
- // Save after tree change
57
  useEffect(() => {
58
  saveTree(tree);
59
  }, [tree]);
60
 
61
- // =================== FILE ACTIONS ===================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  const handleNewFile = () => {
63
- const name = window.prompt("Filename (with extension):");
64
- if (!name) return;
65
- setTree(addFile(tree, name));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  };
67
 
 
68
  const handleNewFolder = () => {
69
- const name = window.prompt("Folder name:");
70
  if (!name) return;
71
- setTree(addFolder(tree, name));
 
 
 
72
  };
73
 
 
74
  const handleRename = () => {
75
  if (!activePath) return;
76
- const newName = window.prompt("New name:", currentFile.name);
77
- if (!newName) return;
78
- setTree(renameNode(tree, activePath, newName));
79
- setActivePath(newName);
 
 
 
 
 
 
 
 
 
80
  };
81
 
 
82
  const handleDelete = () => {
83
  if (!activePath) return;
84
- setTree(deleteNode(tree, activePath));
85
- setActivePath(""); // unselect
 
 
 
 
 
 
 
 
 
 
 
 
86
  };
87
 
 
88
  const downloadFile = () => {
89
- if (!currentFile?.content) return;
90
- const blob = new Blob([currentFile.content], {
91
- type: "text/plain;charset=utf-8",
92
- });
93
  const a = document.createElement("a");
94
  a.href = URL.createObjectURL(blob);
95
- a.download = currentFile.name;
96
  a.click();
97
  };
98
 
99
- // =================== RUN ===================
100
- const handleRun = async () => {
101
- if (!currentFile?.content) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- const selectedLang = langMeta.id;
104
- if (!RUNNABLE_LANGS.includes(selectedLang)) {
105
- setOutput(`⚠️ Cannot run ${selectedLang}.`);
 
 
 
 
 
 
 
 
106
  return;
107
  }
108
 
109
- const res = await runCode(currentFile.content, selectedLang, stdin);
110
  setOutput(res.output || "");
111
  setProblems(res.error ? parseProblems(res.output) : []);
112
  };
113
 
114
- // =================== AI ===================
115
  const handleAskFix = async () => {
116
- if (!currentFile) return;
 
 
 
 
117
  const userHint = prompt.trim() ? `User request: ${prompt}` : "";
118
  const reply = await askAgent(
119
- `Improve, debug, or refactor this ${langMeta.id} file.
120
- ${userHint}
121
- Return ONLY updated code, no explanation.
122
-
123
- CODE:
124
- ${currentFile.content}`
125
  );
126
- updateActiveFileContent(reply);
 
127
  };
128
 
129
  const handleExplainSelection = async () => {
130
- if (!currentFile) return;
 
 
 
 
131
  const editor = editorRef.current;
132
- const selected = editor
133
- .getModel()
134
- .getValueInRange(editor.getSelection());
135
- const code = selected.trim() || currentFile.content;
136
-
137
- const userHint = prompt.trim()
138
- ? `Focus on: ${prompt}`
139
- : "Give a clear and simple explanation.";
140
-
141
  const reply = await askAgent(
142
- `Explain what this ${langMeta.id} code does, any risks, and improvements.
143
- ${userHint}
144
-
145
- CODE:
146
- ${code}`
147
  );
148
  setExplanation(reply);
149
  };
150
 
151
- const updateActiveFileContent = (value) => {
152
- const updated = updateFileContent(tree, activePath, value);
153
- setTree(updated);
154
- };
155
-
156
- // ===== AI Autocomplete Suggestions (Popup) =====
157
  const fetchAiSuggestions = async (code) => {
158
  if (!code?.trim()) return;
159
- const reply = await askAgent(
160
- `Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`
161
- );
162
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
163
  };
164
 
165
- // =================== SEARCH ===================
166
  const handleSearchToggle = () => setSearchOpen(!searchOpen);
167
 
168
  const handleSearchNow = () => {
169
  if (!searchQuery) return;
170
- alert(
171
- `Found occurrences:\n${JSON.stringify(
172
- searchTree(tree, searchQuery),
173
- null,
174
- 2
175
- )}`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  );
177
  };
178
 
179
- // =================== UI ===================
180
  return (
181
- <div
182
- className={`ide-root ${
183
- theme === "vs-dark" ? "ide-dark" : "ide-light"
184
- }`}
185
- >
186
- {/* ==== TOP BAR ==== */}
 
 
 
 
 
187
  <div className="ide-menubar">
188
  <div className="ide-menubar-left">
189
  <span className="ide-logo">⚙️ DevMate IDE</span>
@@ -191,8 +306,10 @@ ${code}`
191
  <button onClick={handleNewFile}>📄 New File</button>
192
  <button onClick={handleNewFolder}>📁 New Folder</button>
193
  <button onClick={handleRename}>✏️ Rename</button>
 
194
  <button onClick={downloadFile}>📥 Download</button>
195
- <button onClick={downloadProjectZip}>📦 ZIP</button>
 
196
  </div>
197
 
198
  <div className="ide-menubar-right">
@@ -200,38 +317,45 @@ ${code}`
200
  <button onClick={handleRun}>▶ Run</button>
201
  <button onClick={handleAskFix}>🤖 Fix</button>
202
  <button onClick={handleExplainSelection}>📖 Explain</button>
203
- <button
204
- onClick={() =>
205
- setTheme(theme === "vs-dark" ? "light" : "vs-dark")
206
- }
207
- >
208
  {theme === "vs-dark" ? "☀️" : "🌙"}
209
  </button>
210
  </div>
211
  </div>
212
 
213
- {/* ==== BODY ==== */}
214
  <div className="ide-body">
215
- {/* ==== TREE VIEW (LEFT) ==== */}
216
  <div className="ide-sidebar">
217
- {renderTree(tree, setActivePath, setContextMenu)}
 
 
 
 
 
 
218
  </div>
219
 
220
- {/* ==== MAIN CENTER (EDITOR + OUTPUT) ==== */}
221
  <div className="ide-main">
222
  <div className="ide-editor-wrapper">
223
  <Editor
224
  height="100%"
225
  theme={theme}
226
  language={langMeta.monaco}
227
- value={currentFile?.content || ""}
228
  onChange={updateActiveFileContent}
229
  onMount={(editor) => (editorRef.current = editor)}
230
- onBlur={() => fetchAiSuggestions(currentFile?.content)}
 
 
 
 
 
231
  />
232
  </div>
233
 
234
- {/* AI Suggestions Tooltip */}
235
  {aiSuggestions.length > 0 && (
236
  <div className="ai-popup">
237
  {aiSuggestions.map((s, i) => (
@@ -239,9 +363,7 @@ ${code}`
239
  key={i}
240
  className="ai-suggest"
241
  onClick={() =>
242
- updateActiveFileContent(
243
- (currentFile?.content || "") + "\n" + s
244
- )
245
  }
246
  >
247
  {s}
@@ -250,7 +372,7 @@ ${code}`
250
  </div>
251
  )}
252
 
253
- {/* Bottom Panels: Output + stdin + Problems */}
254
  <div className="ide-panels">
255
  <pre className="ide-output">{output}</pre>
256
 
@@ -266,7 +388,7 @@ ${code}`
266
  <div>🚨 Problems ({problems.length})</div>
267
  {problems.map((p, i) => (
268
  <div key={i}>
269
- {p.file}:{p.line} — {p.message}
270
  </div>
271
  ))}
272
  </div>
@@ -274,7 +396,7 @@ ${code}`
274
  </div>
275
  </div>
276
 
277
- {/* ==== RIGHT AI PANEL ==== */}
278
  <div className="ide-right-panel">
279
  <div className="ide-ai-header">🤖 AI Assistant</div>
280
 
@@ -282,7 +404,7 @@ ${code}`
282
  <label className="ide-ai-label">Instruction</label>
283
  <textarea
284
  className="ide-agent-textarea"
285
- placeholder="Ask the AI (e.g., 'optimize this function', 'add comments', 'convert to JS'...)"
286
  value={prompt}
287
  onChange={(e) => setPrompt(e.target.value)}
288
  />
@@ -302,7 +424,7 @@ ${code}`
302
  </div>
303
  </div>
304
 
305
- {/* ==== SEARCH PANEL ==== */}
306
  {searchOpen && (
307
  <div className="search-dialog">
308
  <input
@@ -313,44 +435,41 @@ ${code}`
313
  </div>
314
  )}
315
 
316
- {/* ==== RIGHT-CLICK CONTEXT MENU ==== */}
317
  {contextMenu && (
318
  <div
319
  className="ide-context-menu"
320
  style={{ top: contextMenu.y, left: contextMenu.x }}
321
  onMouseLeave={() => setContextMenu(null)}
322
  >
323
- <div onClick={handleRename}>✏️ Rename</div>
324
- <div onClick={handleDelete}>🗑 Delete</div>
325
- <div onClick={downloadFile}>📥 Download</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  </div>
327
  )}
328
  </div>
329
  );
330
  }
331
 
332
- // =======================================================
333
- // Tree Renderer UI
334
- // =======================================================
335
- function renderTree(node, setActivePath, setContextMenu, depth = 0) {
336
- return (
337
- <div key={node.name} style={{ paddingLeft: depth * 10 }}>
338
- <div
339
- className={`tree-item ${node.type}`}
340
- onClick={() => node.type === "file" && setActivePath(node.path)}
341
- onContextMenu={(e) => {
342
- e.preventDefault();
343
- setContextMenu({ x: e.pageX, y: e.pageY, file: node.path });
344
- }}
345
- >
346
- {node.type === "folder" ? "📁" : "📄"} {node.name}
347
- </div>
348
- {node.children &&
349
- node.children.map((child) =>
350
- renderTree(child, setActivePath, setContextMenu, depth + 1)
351
- )}
352
- </div>
353
- );
354
- }
355
-
356
  export default App;
 
35
  // =================== APP ===================
36
  function App() {
37
  const [tree, setTree] = useState(loadTree());
38
+ const [activePath, setActivePath] = useState("main.py"); // selected file or folder path
39
  const [output, setOutput] = useState("");
40
  const [prompt, setPrompt] = useState("");
41
  const [explanation, setExplanation] = useState("");
 
45
  const [searchOpen, setSearchOpen] = useState(false);
46
  const [searchQuery, setSearchQuery] = useState("");
47
  const [aiSuggestions, setAiSuggestions] = useState([]);
48
+ const [contextMenu, setContextMenu] = useState(null); // {x,y,file}
49
  const editorRef = useRef(null);
50
+ const fileInputRef = useRef(null);
51
 
52
+ // Always persist tree on change
 
 
 
 
 
53
  useEffect(() => {
54
  saveTree(tree);
55
  }, [tree]);
56
 
57
+ // helpers
58
+ const currentNode = getNodeByPath(tree, activePath);
59
+ const langMeta =
60
+ LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
61
+ LANGUAGE_OPTIONS[0];
62
+
63
+ // ---------- Tree utilities ----------
64
+ const collectFolderPaths = (node, acc = []) => {
65
+ if (!node) return acc;
66
+ if (node.type === "folder") acc.push(node.path || "");
67
+ node.children?.forEach((c) => collectFolderPaths(c, acc));
68
+ return acc;
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;
77
+
78
+ // Determine default parent
79
+ const selected = getNodeByPath(tree, activePath);
80
+ let parentPath = "";
81
+ if (selected?.type === "folder") parentPath = selected.path;
82
+ else if (selected?.type === "file") {
83
+ const parts = selected.path.split("/").slice(0, -1);
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(
91
+ `Parent folder (enter path). Available:\n${folders.join("\n")}\n\nLeave empty for root.`,
92
+ suggestion
93
+ );
94
+ const targetParent = chosen == null ? parentPath : (chosen.trim() || "");
95
+
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;
108
+ const selected = getNodeByPath(tree, activePath);
109
+ const parentPath = selected && selected.type === "folder" ? selected.path : "";
110
+ const updated = addFolder(tree, name, parentPath);
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);
118
+ if (!node) return;
119
+ const newName = window.prompt("New name:", node.name);
120
+ if (!newName || newName === node.name) return;
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("/");
128
+ const newPath = (parent ? parent + "/" : "") + newName;
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);
136
+ if (!node) return;
137
+
138
+ if (node.type === "folder" && node.children?.length > 0) {
139
+ const ok = window.confirm(`Folder "${node.name}" has ${node.children.length} items. Delete anyway?`);
140
+ if (!ok) return;
141
+ } else {
142
+ const ok = window.confirm(`Delete "${node.name}"?`);
143
+ if (!ok) return;
144
+ }
145
+
146
+ const updated = deleteNode(tree, activePath);
147
+ setTree(updated);
148
+ setActivePath(""); // deselect / fallback
149
  };
150
 
151
+ // Download single file
152
  const downloadFile = () => {
153
+ const node = getNodeByPath(tree, activePath);
154
+ if (!node || node.type !== "file") return;
155
+ const blob = new Blob([node.content || ""], { type: "text/plain;charset=utf-8" });
 
156
  const a = document.createElement("a");
157
  a.href = URL.createObjectURL(blob);
158
+ a.download = node.name;
159
  a.click();
160
  };
161
 
162
+ // Import file from user's machine into selected folder
163
+ const handleImportFileClick = () => fileInputRef.current?.click();
164
+
165
+ const handleFileInputChange = async (e) => {
166
+ const f = e.target.files?.[0];
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);
183
+ setTree(finalTree);
184
+ setActivePath(newPath);
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") {
193
+ setOutput("Select a file to run.");
194
+ return;
195
+ }
196
+ const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
197
+ if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
198
+ setOutput(`⚠️ Run not supported for this file type.`);
199
  return;
200
  }
201
 
202
+ const res = await runCode(node.content, selectedLang, stdin);
203
  setOutput(res.output || "");
204
  setProblems(res.error ? parseProblems(res.output) : []);
205
  };
206
 
 
207
  const handleAskFix = async () => {
208
+ const node = getNodeByPath(tree, activePath);
209
+ if (!node || node.type !== "file") {
210
+ setOutput("Select a file to apply fix.");
211
+ return;
212
+ }
213
  const userHint = prompt.trim() ? `User request: ${prompt}` : "";
214
  const reply = await askAgent(
215
+ `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}`
 
 
 
 
 
216
  );
217
+ const updatedTree = updateFileContent(tree, node.path, reply);
218
+ setTree(updatedTree);
219
  };
220
 
221
  const handleExplainSelection = async () => {
222
+ const node = getNodeByPath(tree, activePath);
223
+ if (!node || node.type !== "file") {
224
+ setExplanation("Select a file to explain.");
225
+ return;
226
+ }
227
  const editor = editorRef.current;
228
+ let selectedCode = "";
229
+ try {
230
+ selectedCode = editor?.getModel()?.getValueInRange(editor.getSelection()) || "";
231
+ } catch {}
232
+ const code = selectedCode.trim() || node.content;
233
+ const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation.";
 
 
 
234
  const reply = await askAgent(
235
+ `Explain what this code does, any risks, and improvements.\n${userHint}\n\nCODE:\n${code}`
 
 
 
 
236
  );
237
  setExplanation(reply);
238
  };
239
 
240
+ // AI suggestions for continuation (simple)
 
 
 
 
 
241
  const fetchAiSuggestions = async (code) => {
242
  if (!code?.trim()) return;
243
+ const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
 
 
244
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
245
  };
246
 
247
+ // ---------- Search ----------
248
  const handleSearchToggle = () => setSearchOpen(!searchOpen);
249
 
250
  const handleSearchNow = () => {
251
  if (!searchQuery) return;
252
+ const results = searchTree(tree, searchQuery);
253
+ alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
254
+ };
255
+
256
+ // ---------- Editor change ----------
257
+ const updateActiveFileContent = (value) => {
258
+ const node = getNodeByPath(tree, activePath);
259
+ if (!node) return;
260
+ const updated = updateFileContent(tree, activePath, value ?? "");
261
+ setTree(updated);
262
+ };
263
+
264
+ // ---------- Render Tree (moved inside component so it sees activePath) ----------
265
+ const renderTree = (node, depth = 0) => {
266
+ const isActive = node.path === activePath;
267
+ return (
268
+ <div key={node.path || node.name} style={{ paddingLeft: depth * 10 }}>
269
+ <div
270
+ className={`tree-item ${node.type} ${isActive ? "ide-file-item-active" : ""}`}
271
+ onClick={() => setActivePath(node.path)}
272
+ onContextMenu={(e) => {
273
+ e.preventDefault();
274
+ setActivePath(node.path);
275
+ setContextMenu({ x: e.pageX, y: e.pageY, file: node.path });
276
+ }}
277
+ style={{ display: "flex", alignItems: "center", gap: 8 }}
278
+ >
279
+ <span style={{ width: 18 }}>{node.type === "folder" ? "📁" : "📄"}</span>
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>
 
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">
 
317
  <button onClick={handleRun}>▶ Run</button>
318
  <button onClick={handleAskFix}>🤖 Fix</button>
319
  <button onClick={handleExplainSelection}>📖 Explain</button>
320
+ <button onClick={() => setTheme((t) => (t === "vs-dark" ? "light" : "vs-dark"))}>
 
 
 
 
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) */}
340
  <div className="ide-main">
341
  <div className="ide-editor-wrapper">
342
  <Editor
343
  height="100%"
344
  theme={theme}
345
  language={langMeta.monaco}
346
+ value={currentNode?.content || ""}
347
  onChange={updateActiveFileContent}
348
  onMount={(editor) => (editorRef.current = editor)}
349
+ onBlur={() => fetchAiSuggestions(currentNode?.content)}
350
+ options={{
351
+ minimap: { enabled: true },
352
+ fontSize: 14,
353
+ scrollBeyondLastLine: false,
354
+ }}
355
  />
356
  </div>
357
 
358
+ {/* AI inline suggestions popup */}
359
  {aiSuggestions.length > 0 && (
360
  <div className="ai-popup">
361
  {aiSuggestions.map((s, i) => (
 
363
  key={i}
364
  className="ai-suggest"
365
  onClick={() =>
366
+ updateActiveFileContent((currentNode?.content || "") + "\n" + s)
 
 
367
  }
368
  >
369
  {s}
 
372
  </div>
373
  )}
374
 
375
+ {/* Bottom panels */}
376
  <div className="ide-panels">
377
  <pre className="ide-output">{output}</pre>
378
 
 
388
  <div>🚨 Problems ({problems.length})</div>
389
  {problems.map((p, i) => (
390
  <div key={i}>
391
+ {p.path}:{p.line} — {p.message}
392
  </div>
393
  ))}
394
  </div>
 
396
  </div>
397
  </div>
398
 
399
+ {/* Right AI panel */}
400
  <div className="ide-right-panel">
401
  <div className="ide-ai-header">🤖 AI Assistant</div>
402
 
 
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
  />
 
424
  </div>
425
  </div>
426
 
427
+ {/* Search dialog */}
428
  {searchOpen && (
429
  <div className="search-dialog">
430
  <input
 
435
  </div>
436
  )}
437
 
438
+ {/* Context menu */}
439
  {contextMenu && (
440
  <div
441
  className="ide-context-menu"
442
  style={{ top: contextMenu.y, left: contextMenu.x }}
443
  onMouseLeave={() => setContextMenu(null)}
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>
472
  );
473
  }
474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  export default App;
src/fileStore.js CHANGED
@@ -1,8 +1,17 @@
1
  // fileStore.js
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- const STORAGE_KEY = "devmate_ide_tree";
4
-
5
- // Default starter tree
6
  const defaultTree = {
7
  type: "folder",
8
  name: "root",
@@ -15,111 +24,188 @@ const defaultTree = {
15
  content: "# Python\nprint('Hello from IDE')",
16
  },
17
  {
18
- type: "file",
19
- name: "script.js",
20
- path: "script.js",
21
- content: "console.log('Hello from JS');",
 
 
 
 
 
 
 
22
  },
23
  ],
24
  };
25
 
26
- // ============ HELPERS ============
27
-
28
  export function loadTree() {
29
  try {
30
- const data = localStorage.getItem(STORAGE_KEY);
31
- return data ? JSON.parse(data) : defaultTree;
32
- } catch {
 
33
  return defaultTree;
34
  }
35
  }
36
 
37
  export function saveTree(tree) {
38
- localStorage.setItem(STORAGE_KEY, JSON.stringify(tree));
 
 
 
 
39
  }
40
 
41
- export function getNodeByPath(node, path) {
42
- if (!path) return null;
43
- if (node.path === path) return node;
44
- if (!node.children) return null;
45
- for (let c of node.children) {
46
- const result = getNodeByPath(c, path);
47
- if (result) return result;
 
 
 
 
 
 
 
 
 
 
48
  }
49
- return null;
50
  }
51
 
52
- function buildPath(parent, name) {
53
- return parent.path ? `${parent.path}/${name}` : name;
 
54
  }
55
 
56
- // ============ ADD ============
 
 
 
 
 
57
 
58
- export function addFile(tree, name) {
59
- const newTree = JSON.parse(JSON.stringify(tree));
60
- newTree.children.push({
61
- type: "file",
62
- name,
63
- path: name,
64
- content: `// ${name}`,
65
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return newTree;
67
  }
68
 
69
- export function addFolder(tree, name) {
70
- const newTree = JSON.parse(JSON.stringify(tree));
71
- newTree.children.push({
72
- type: "folder",
73
- name,
74
- path: name,
75
- children: [],
76
- });
 
 
 
 
 
 
 
 
77
  return newTree;
78
  }
79
 
80
- // ============ DELETE ============
81
-
82
  export function deleteNode(tree, path) {
83
- const clone = JSON.parse(JSON.stringify(tree));
84
- clone.children = clone.children.filter((c) => c.path !== path);
85
- return clone;
 
 
 
 
 
 
86
  }
87
 
88
- // ============ RENAME ============
89
-
90
  export function renameNode(tree, path, newName) {
91
- const clone = JSON.parse(JSON.stringify(tree));
92
- function renameRec(node) {
93
- if (node.path === path) {
94
- node.name = newName;
95
- node.path = newName;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
- node.children?.forEach(renameRec);
98
  }
99
- renameRec(clone);
100
- return clone;
101
  }
102
 
103
- // ============ UPDATE CONTENT ============
104
-
105
  export function updateFileContent(tree, path, content) {
106
- const clone = JSON.parse(JSON.stringify(tree));
107
- function update(node) {
108
- if (node.path === path && node.type === "file") {
109
- node.content = content;
110
- }
111
- node.children?.forEach(update);
112
  }
113
- update(clone);
114
- return clone;
115
  }
116
 
117
- // ============ SEARCH ============
118
-
119
- export function searchTree(node, term, result = []) {
120
- if (node.type === "file" && node.content?.includes(term)) {
121
- result.push({ file: node.name, path: node.path });
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
- node.children?.forEach((c) => searchTree(c, term, result));
124
- return result;
125
  }
 
1
  // fileStore.js
2
+ const STORAGE_KEY = "devmate_ide_tree_v2";
3
+
4
+ /**
5
+ * Tree node structure:
6
+ * {
7
+ * type: "folder" | "file",
8
+ * name: "src" or "main.py",
9
+ * path: "src" or "src/main.py",
10
+ * children: [ ... ] // only for folders
11
+ * content: "..." // only for files
12
+ * }
13
+ */
14
 
 
 
 
15
  const defaultTree = {
16
  type: "folder",
17
  name: "root",
 
24
  content: "# Python\nprint('Hello from IDE')",
25
  },
26
  {
27
+ type: "folder",
28
+ name: "src",
29
+ path: "src",
30
+ children: [
31
+ {
32
+ type: "file",
33
+ name: "script.js",
34
+ path: "src/script.js",
35
+ content: "console.log('Hello from src');",
36
+ },
37
+ ],
38
  },
39
  ],
40
  };
41
 
42
+ // ---------- persistence ----------
 
43
  export function loadTree() {
44
  try {
45
+ const raw = localStorage.getItem(STORAGE_KEY);
46
+ return raw ? JSON.parse(raw) : defaultTree;
47
+ } catch (e) {
48
+ console.warn("loadTree failed:", e);
49
  return defaultTree;
50
  }
51
  }
52
 
53
  export function saveTree(tree) {
54
+ try {
55
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(tree));
56
+ } catch (e) {
57
+ console.warn("saveTree failed:", e);
58
+ }
59
  }
60
 
61
+ // ---------- traversal helpers ----------
62
+ function clone(obj) {
63
+ return JSON.parse(JSON.stringify(obj));
64
+ }
65
+
66
+ // find node and parent by path
67
+ export function findNodeAndParent(root, path) {
68
+ if (path === "" || path == null) return { node: root, parent: null };
69
+ const parts = path.split("/").filter(Boolean);
70
+ let node = root;
71
+ let parent = null;
72
+ for (let i = 0; i < parts.length; i++) {
73
+ const part = parts[i];
74
+ parent = node;
75
+ if (!parent.children) return { node: null, parent: null };
76
+ node = parent.children.find((c) => c.name === part);
77
+ if (!node) return { node: null, parent: null };
78
  }
79
+ return { node, parent };
80
  }
81
 
82
+ export function getNodeByPath(root, path) {
83
+ const r = findNodeAndParent(root, path);
84
+ return r.node || null;
85
  }
86
 
87
+ // build child path
88
+ function joinPath(parentPath, name) {
89
+ if (!parentPath) return name;
90
+ if (!name) return parentPath;
91
+ return parentPath + "/" + name;
92
+ }
93
 
94
+ // ---------- CRUD operations ----------
95
+
96
+ // Add file under parentPath (string). If parentPath points to file, use its parent.
97
+ export function addFile(tree, filename, parentPath = "") {
98
+ const newTree = clone(tree);
99
+ // find parent
100
+ const { node: parent } = findNodeAndParent(newTree, parentPath);
101
+ const target = parent?.type === "folder" ? parent : newTree;
102
+ // ensure unique name
103
+ let name = filename;
104
+ const exists = (name) =>
105
+ target.children && target.children.some((c) => c.name === name);
106
+ let idx = 1;
107
+ const base = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
108
+ const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : "";
109
+ while (exists(name)) {
110
+ name = `${base}_${idx++}${ext}`;
111
+ }
112
+ const path = joinPath(target.path, name);
113
+ const newNode = { type: "file", name, path, content: `// ${name}\n` };
114
+ target.children = target.children || [];
115
+ target.children.push(newNode);
116
  return newTree;
117
  }
118
 
119
+ // Add folder under parentPath
120
+ export function addFolder(tree, folderName, parentPath = "") {
121
+ const newTree = clone(tree);
122
+ const { node: parent } = findNodeAndParent(newTree, parentPath);
123
+ const target = parent?.type === "folder" ? parent : newTree;
124
+ let name = folderName;
125
+ const exists = (name) =>
126
+ target.children && target.children.some((c) => c.name === name && c.type === "folder");
127
+ let idx = 1;
128
+ while (exists(name)) {
129
+ name = `${folderName}_${idx++}`;
130
+ }
131
+ const path = joinPath(target.path, name);
132
+ const newNode = { type: "folder", name, path, children: [] };
133
+ target.children = target.children || [];
134
+ target.children.push(newNode);
135
  return newTree;
136
  }
137
 
138
+ // Delete node (file or folder) at given path (recursive for folders)
 
139
  export function deleteNode(tree, path) {
140
+ const newTree = clone(tree);
141
+ if (!path) return newTree; // cannot delete root
142
+ const parts = path.split("/").filter(Boolean);
143
+ const nameToDelete = parts.pop();
144
+ const parentPath = parts.join("/");
145
+ const { node: parent } = findNodeAndParent(newTree, parentPath);
146
+ if (!parent || !parent.children) return newTree;
147
+ parent.children = parent.children.filter((c) => c.name !== nameToDelete);
148
+ return newTree;
149
  }
150
 
151
+ // Rename node: change name and update paths recursively for children
 
152
  export function renameNode(tree, path, newName) {
153
+ const newTree = clone(tree);
154
+ const { node, parent } = findNodeAndParent(newTree, path);
155
+ if (!node || !parent) {
156
+ // special case root rename not allowed
157
+ if (node && !parent) return newTree;
158
+ return newTree;
159
+ }
160
+ // ensure no duplicate under parent
161
+ if (parent.children.some((c) => c.name === newName && c.path !== node.path)) {
162
+ // collision: abort
163
+ return newTree;
164
+ }
165
+
166
+ const oldPath = node.path;
167
+ node.name = newName;
168
+ const newPath = joinPath(parent.path, newName);
169
+ // update path recursively
170
+ function updatePaths(n, currPath) {
171
+ n.path = currPath;
172
+ if (n.children) {
173
+ for (let child of n.children) {
174
+ const childNewPath = currPath ? currPath + "/" + child.name : child.name;
175
+ updatePaths(child, childNewPath);
176
+ }
177
  }
 
178
  }
179
+ updatePaths(node, newPath);
180
+ return newTree;
181
  }
182
 
183
+ // Update file content
 
184
  export function updateFileContent(tree, path, content) {
185
+ const newTree = clone(tree);
186
+ const { node } = findNodeAndParent(newTree, path);
187
+ if (node && node.type === "file") {
188
+ node.content = content;
 
 
189
  }
190
+ return newTree;
 
191
  }
192
 
193
+ // Search within files (returns array of {path, file, excerpt})
194
+ export function searchTree(node, term, results = []) {
195
+ if (!node) return results;
196
+ if (node.type === "file" && node.content && node.content.includes(term)) {
197
+ results.push({
198
+ path: node.path,
199
+ file: node.name,
200
+ excerpt: node.content
201
+ .split("\n")
202
+ .filter((l) => l.includes(term))
203
+ .slice(0, 5)
204
+ .join("\n"),
205
+ });
206
+ }
207
+ if (node.children) {
208
+ for (const c of node.children) searchTree(c, term, results);
209
  }
210
+ return results;
 
211
  }