FrederickSundeep commited on
Commit
07044b4
·
1 Parent(s): 34104e6

commit initial 09-12-2025 29

Browse files
Files changed (1) hide show
  1. src/App.js +44 -80
src/App.js CHANGED
@@ -17,7 +17,7 @@ import { downloadProjectZip } from "./zipExport";
17
  import { parseProblems } from "./problemParser";
18
  import "./App.css";
19
  import "xterm/css/xterm.css";
20
- import XTerm from "./Terminal"; // <-- your terminal component
21
 
22
  // =================== SUPPORTED LANGUAGES ===================
23
  const LANGUAGE_OPTIONS = [
@@ -38,20 +38,20 @@ const RUNNABLE_LANGS = ["python", "javascript", "java"];
38
  function outputLooksForInput(output) {
39
  if (!output) return false;
40
  const o = output.toString();
41
- // common prompt words / patterns and trailing prompt char
42
  const patterns = [
43
  /enter.*:/i,
44
  /input.*:/i,
45
  /please enter/i,
46
- /scanner/i, // java Scanner exceptions or prompts
47
  /press enter/i,
48
- /: $/, // ends with colon + space (e.g. "Enter number: ")
49
- /:\n$/, // ends with colon + newline
50
- /> $/, // trailing >
51
  ];
52
  return patterns.some((p) => p.test(o));
53
  }
54
 
 
55
  function codeNeedsInput(code, langId) {
56
  if (!code) return false;
57
  try {
@@ -74,44 +74,42 @@ function codeNeedsInput(code, langId) {
74
  // =================== APP ===================
75
  function App() {
76
  const [tree, setTree] = useState(loadTree());
77
- const [activePath, setActivePath] = useState("main.py"); // selected file or folder path
78
 
79
- // Terminal-ish state
80
- const [accumStdin, setAccumStdin] = useState(""); // accumulated stdin passed to backend
81
- const [awaitingInput, setAwaitingInput] = useState(false); // true if program waiting for input
82
- const [output, setOutput] = useState(""); // last raw output (passed to XTerm)
83
  const [prompt, setPrompt] = useState("");
84
  const [explanation, setExplanation] = useState("");
85
 
86
- // legacy/other UI state
87
  const [terminalInput, setTerminalInput] = useState("");
88
- const [stdin, setStdin] = useState(""); // optional single-run input
89
  const [problems, setProblems] = useState([]);
90
  const [theme, setTheme] = useState("vs-dark");
91
  const [searchOpen, setSearchOpen] = useState(false);
92
  const [searchQuery, setSearchQuery] = useState("");
93
  const [aiSuggestions, setAiSuggestions] = useState([]);
94
- const [contextMenu, setContextMenu] = useState(null); // {x,y,file}
95
  const editorRef = useRef(null);
96
  const fileInputRef = useRef(null);
97
 
98
- // NEW: loading states
99
  const [isRunning, setIsRunning] = useState(false);
100
  const [isFixing, setIsFixing] = useState(false);
101
  const [isExplaining, setIsExplaining] = useState(false);
102
 
103
- // Always persist tree on change
104
  useEffect(() => {
105
  saveTree(tree);
106
  }, [tree]);
107
 
108
- // helpers
109
  const currentNode = getNodeByPath(tree, activePath);
110
  const langMeta =
111
  LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
112
  LANGUAGE_OPTIONS[0];
113
 
114
- // ---------- TREE helpers ----------
115
  const collectFolderPaths = (node, acc = []) => {
116
  if (!node) return acc;
117
  if (node.type === "folder") acc.push(node.path || "");
@@ -124,7 +122,6 @@ function App() {
124
  const filename = window.prompt("Filename (with extension):", "untitled.js");
125
  if (!filename) return;
126
 
127
- // Determine default parent
128
  const selected = getNodeByPath(tree, activePath);
129
  let parentPath = "";
130
  if (selected?.type === "folder") parentPath = selected.path;
@@ -201,7 +198,7 @@ function App() {
201
  a.click();
202
  };
203
 
204
- // Import file from user's machine into selected folder
205
  const handleImportFileClick = () => fileInputRef.current?.click();
206
 
207
  const handleFileInputChange = async (e) => {
@@ -224,9 +221,7 @@ function App() {
224
 
225
  // ---------- Terminal helpers ----------
226
  const resetTerminal = (keepAccum = false) => {
227
- // We clear output state (XTerm will reflect this)
228
  setOutput("");
229
- // only reset accumulated stdin if caller explicitly wants to clear it
230
  if (!keepAccum) {
231
  setAccumStdin("");
232
  }
@@ -234,16 +229,13 @@ function App() {
234
  setTerminalInput("");
235
  };
236
 
237
- // runCodeWithUpdatedInput:
238
- // called when a line is typed in XTerm (onData)
239
  const runCodeWithUpdatedInput = async (inputLine) => {
240
- // echoing is handled by XTerm itself; here we update accumStdin and run
241
  const newAccum = (accumStdin || "") + inputLine + "\n";
242
  setAccumStdin(newAccum);
243
 
244
  const node = getNodeByPath(tree, activePath);
245
  if (!node || node.type !== "file") {
246
- // write a small error into output so XTerm displays it
247
  setOutput((prev) => prev + "\n[Error] No file selected to run.");
248
  setAwaitingInput(false);
249
  return;
@@ -275,7 +267,7 @@ function App() {
275
  }
276
  };
277
 
278
- // ---------- Run (initial) ----------
279
  const handleRun = async () => {
280
  const node = getNodeByPath(tree, activePath);
281
  if (!node || node.type !== "file") {
@@ -288,37 +280,27 @@ function App() {
288
  return;
289
  }
290
 
291
- // If code likely needs input and accumStdin is empty, ask for initial input
292
  const needs = codeNeedsInput(node.content, selectedLang);
293
  let stdinToSend = accumStdin || stdin || "";
294
 
 
295
  if (needs && !stdinToSend) {
296
- const userText = window.prompt(
297
- "This program appears to request console input (e.g. input() / Scanner).\n\nEnter input lines use Enter for new lines or paste multi-line input. Leave empty to run without input.",
298
- ""
299
- );
300
-
301
- if (userText === null) {
302
- // user cancelled prompt -> do nothing (abort run)
303
- setOutput((prev) => prev + "\n[Run cancelled by user]");
304
- return;
305
- }
306
-
307
- // normalize any literal "\n" sequences into real newlines (helps users pasting)
308
- const prepared = userText.replace(/\\n/g, "\n");
309
- stdinToSend = prepared;
310
- // set accumStdin state (so subsequent interactive sends append to it)
311
- setAccumStdin(prepared);
312
  }
313
 
314
- // start fresh terminal output but keep accumulated stdin
315
- resetTerminal(true);
316
- setOutput(`[Running with stdin length=${stdinToSend ? stdinToSend.length : 0}]\n`);
317
-
318
  setIsRunning(true);
319
  setProblems([]);
320
  try {
321
- // IMPORTANT: use local stdinToSend (not state) so the run uses the value right away
322
  const res = await runCode(node.content, selectedLang, stdinToSend);
323
  const out = res.output ?? "";
324
  setOutput(out);
@@ -331,15 +313,17 @@ function App() {
331
  } catch (err) {
332
  const e = String(err);
333
  setOutput(e);
334
- setAwaitingInput(false);
 
335
  } finally {
336
  setIsRunning(false);
337
  }
338
  };
339
 
340
- // Called when user types input into the legacy terminal input box and presses Send
341
  const sendTerminalInput = async () => {
342
- if (!currentNode || currentNode.type !== "file") {
 
343
  setOutput((prev) => prev + "\nNo file selected.");
344
  setAwaitingInput(false);
345
  return;
@@ -352,8 +336,8 @@ function App() {
352
  setTerminalInput("");
353
  setIsRunning(true);
354
  try {
355
- const selectedLang = LANGUAGE_OPTIONS.find((l) => currentNode.name.endsWith(l.ext))?.id;
356
- const res = await runCode(currentNode.content, selectedLang, newAccum);
357
  const out = res.output ?? "";
358
  setOutput(out);
359
  setProblems(res.error ? parseProblems(res.output) : []);
@@ -370,7 +354,6 @@ function App() {
370
  }
371
  };
372
 
373
- // Allow pressing Enter to send input for legacy input box
374
  const onTerminalKeyDown = (e) => {
375
  if (e.key === "Enter") {
376
  e.preventDefault();
@@ -427,14 +410,14 @@ function App() {
427
  }
428
  };
429
 
430
- // AI suggestions for continuation (simple)
431
  const fetchAiSuggestions = async (code) => {
432
  if (!code?.trim()) return;
433
  try {
434
  const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
435
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
436
  } catch {
437
- // ignore suggestion errors
438
  }
439
  };
440
 
@@ -447,7 +430,7 @@ function App() {
447
  alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
448
  };
449
 
450
- // ---------- Editor change ----------
451
  const updateActiveFileContent = (value) => {
452
  const node = getNodeByPath(tree, activePath);
453
  if (!node) return;
@@ -455,7 +438,7 @@ function App() {
455
  setTree(updated);
456
  };
457
 
458
- // ---------- Render Tree (inside component) ----------
459
  const renderTree = (node, depth = 0) => {
460
  const isActive = node.path === activePath;
461
  return (
@@ -479,16 +462,13 @@ function App() {
479
  );
480
  };
481
 
482
- // any loading?
483
  const anyLoading = isRunning || isFixing || isExplaining;
484
 
485
- // ---------- JSX UI ----------
486
  return (
487
  <div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}>
488
- {/* Hidden file input for import */}
489
  <input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
490
 
491
- {/* Top menu */}
492
  <div className="ide-menubar">
493
  <div className="ide-menubar-left">
494
  <span className="ide-logo">⚙️ DevMate IDE</span>
@@ -513,16 +493,13 @@ function App() {
513
  </div>
514
  </div>
515
 
516
- {/* Indeterminate progress bar under menubar when busy */}
517
  {anyLoading && (
518
  <div className="ide-progress-wrap">
519
  <div className="ide-progress" />
520
  </div>
521
  )}
522
 
523
- {/* Body */}
524
  <div className="ide-body">
525
- {/* Sidebar */}
526
  <div className="ide-sidebar">
527
  <div className="ide-sidebar-header">
528
  <span>EXPLORER</span>
@@ -531,7 +508,6 @@ function App() {
531
  <div className="ide-file-list" style={{ padding: 6 }}>{renderTree(tree)}</div>
532
  </div>
533
 
534
- {/* Main (editor + bottom panels) */}
535
  <div className="ide-main">
536
  <div className="ide-editor-wrapper">
537
  <Editor
@@ -546,32 +522,25 @@ function App() {
546
  />
547
  </div>
548
 
549
- {/* AI inline suggestions popup */}
550
  {aiSuggestions.length > 0 && (
551
  <div className="ai-popup">
552
  {aiSuggestions.map((s, i) => (
553
- <div key={i} className="ai-suggest" onClick={() => updateActiveFileContent((currentNode?.content || "") + "\n" + s)}>{s}</div>
554
  ))}
555
  </div>
556
  )}
557
 
558
- {/* Bottom panels */}
559
  <div className="ide-panels">
560
- {/* Terminal (XTerm) */}
561
  <div style={{ marginBottom: 8 }}>
562
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
563
 
564
- {/* XTerm shows the terminal and accepts input inline.
565
- onData calls runCodeWithUpdatedInput which appends stdin and re-runs */}
566
  <XTerm
567
  output={output}
568
  onData={(line) => {
569
- // runCodeWithUpdatedInput handles accumulation and re-run
570
  runCodeWithUpdatedInput(line);
571
  }}
572
  />
573
 
574
- {/* legacy input fallback - still available if user prefers box */}
575
  {!awaitingInput && (
576
  <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
577
  <input className="ide-input-box" placeholder="(Optional) Program input for single-run" value={stdin} onChange={(e) => setStdin(e.target.value)} />
@@ -579,7 +548,6 @@ function App() {
579
  </div>
580
  )}
581
 
582
- {/* If using legacy input box to send while awaitingInput */}
583
  {awaitingInput && (
584
  <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
585
  <input
@@ -595,7 +563,6 @@ function App() {
595
  )}
596
  </div>
597
 
598
- {/* Problems */}
599
  {problems.length > 0 && (
600
  <div className="ide-problems-panel">
601
  <div>🚨 Problems ({problems.length})</div>
@@ -605,7 +572,6 @@ function App() {
605
  </div>
606
  </div>
607
 
608
- {/* Right AI panel */}
609
  <div className="ide-right-panel">
610
  <div className="ide-ai-header">🤖 AI Assistant</div>
611
 
@@ -628,7 +594,6 @@ function App() {
628
  </div>
629
  </div>
630
 
631
- {/* Search dialog */}
632
  {searchOpen && (
633
  <div className="search-dialog">
634
  <input placeholder="Search text..." onChange={(e) => setSearchQuery(e.target.value)} />
@@ -636,7 +601,6 @@ function App() {
636
  </div>
637
  )}
638
 
639
- {/* Context menu */}
640
  {contextMenu && (
641
  <div className="ide-context-menu" style={{ top: contextMenu.y, left: contextMenu.x }} onMouseLeave={() => setContextMenu(null)}>
642
  <div onClick={() => { setContextMenu(null); handleRename(); }}>✏️ Rename</div>
 
17
  import { parseProblems } from "./problemParser";
18
  import "./App.css";
19
  import "xterm/css/xterm.css";
20
+ import XTerm from "./Terminal"; // your xterm wrapper component
21
 
22
  // =================== SUPPORTED LANGUAGES ===================
23
  const LANGUAGE_OPTIONS = [
 
38
  function outputLooksForInput(output) {
39
  if (!output) return false;
40
  const o = output.toString();
 
41
  const patterns = [
42
  /enter.*:/i,
43
  /input.*:/i,
44
  /please enter/i,
45
+ /scanner/i,
46
  /press enter/i,
47
+ /: $/,
48
+ /:\n$/,
49
+ /> $/,
50
  ];
51
  return patterns.some((p) => p.test(o));
52
  }
53
 
54
+ // Heuristic: detect input calls inside source
55
  function codeNeedsInput(code, langId) {
56
  if (!code) return false;
57
  try {
 
74
  // =================== APP ===================
75
  function App() {
76
  const [tree, setTree] = useState(loadTree());
77
+ const [activePath, setActivePath] = useState("main.py");
78
 
79
+ // Terminal state
80
+ const [accumStdin, setAccumStdin] = useState("");
81
+ const [awaitingInput, setAwaitingInput] = useState(false);
82
+ const [output, setOutput] = useState("");
83
  const [prompt, setPrompt] = useState("");
84
  const [explanation, setExplanation] = useState("");
85
 
86
+ // legacy and other UI
87
  const [terminalInput, setTerminalInput] = useState("");
88
+ const [stdin, setStdin] = useState("");
89
  const [problems, setProblems] = useState([]);
90
  const [theme, setTheme] = useState("vs-dark");
91
  const [searchOpen, setSearchOpen] = useState(false);
92
  const [searchQuery, setSearchQuery] = useState("");
93
  const [aiSuggestions, setAiSuggestions] = useState([]);
94
+ const [contextMenu, setContextMenu] = useState(null);
95
  const editorRef = useRef(null);
96
  const fileInputRef = useRef(null);
97
 
98
+ // loading flags
99
  const [isRunning, setIsRunning] = useState(false);
100
  const [isFixing, setIsFixing] = useState(false);
101
  const [isExplaining, setIsExplaining] = useState(false);
102
 
 
103
  useEffect(() => {
104
  saveTree(tree);
105
  }, [tree]);
106
 
 
107
  const currentNode = getNodeByPath(tree, activePath);
108
  const langMeta =
109
  LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
110
  LANGUAGE_OPTIONS[0];
111
 
112
+ // ---------- Tree helpers ----------
113
  const collectFolderPaths = (node, acc = []) => {
114
  if (!node) return acc;
115
  if (node.type === "folder") acc.push(node.path || "");
 
122
  const filename = window.prompt("Filename (with extension):", "untitled.js");
123
  if (!filename) return;
124
 
 
125
  const selected = getNodeByPath(tree, activePath);
126
  let parentPath = "";
127
  if (selected?.type === "folder") parentPath = selected.path;
 
198
  a.click();
199
  };
200
 
201
+ // import file
202
  const handleImportFileClick = () => fileInputRef.current?.click();
203
 
204
  const handleFileInputChange = async (e) => {
 
221
 
222
  // ---------- Terminal helpers ----------
223
  const resetTerminal = (keepAccum = false) => {
 
224
  setOutput("");
 
225
  if (!keepAccum) {
226
  setAccumStdin("");
227
  }
 
229
  setTerminalInput("");
230
  };
231
 
232
+ // On-line input from XTerm -> append and re-run
 
233
  const runCodeWithUpdatedInput = async (inputLine) => {
 
234
  const newAccum = (accumStdin || "") + inputLine + "\n";
235
  setAccumStdin(newAccum);
236
 
237
  const node = getNodeByPath(tree, activePath);
238
  if (!node || node.type !== "file") {
 
239
  setOutput((prev) => prev + "\n[Error] No file selected to run.");
240
  setAwaitingInput(false);
241
  return;
 
267
  }
268
  };
269
 
270
+ // Initial run NO browser prompt; rely on XTerm and accumStdin
271
  const handleRun = async () => {
272
  const node = getNodeByPath(tree, activePath);
273
  if (!node || node.type !== "file") {
 
280
  return;
281
  }
282
 
283
+ // If code likely needs input and accumStdin empty, do NOT prompt — use XTerm
284
  const needs = codeNeedsInput(node.content, selectedLang);
285
  let stdinToSend = accumStdin || stdin || "";
286
 
287
+ // If it needs input but accumStdin is empty, set awaitingInput true so user can type in XTerm
288
  if (needs && !stdinToSend) {
289
+ // Inform user in terminal output and allow them to type in XTerm
290
+ setOutput("[Interactive program detectedtype input directly into the terminal]");
291
+ setAwaitingInput(true);
292
+ // We still attempt a run with empty stdin to let the program show initial prompts if any.
293
+ // This may produce EOF errors for some programs; user can then type into the terminal to continue.
294
+ } else {
295
+ // reset terminal output for fresh run but keep accumulated stdin
296
+ resetTerminal(true);
297
+ setOutput(`[Running with stdin length=${stdinToSend ? stdinToSend.length : 0}]\n`);
 
 
 
 
 
 
 
298
  }
299
 
 
 
 
 
300
  setIsRunning(true);
301
  setProblems([]);
302
  try {
303
+ // Use local stdinToSend to avoid setState race
304
  const res = await runCode(node.content, selectedLang, stdinToSend);
305
  const out = res.output ?? "";
306
  setOutput(out);
 
313
  } catch (err) {
314
  const e = String(err);
315
  setOutput(e);
316
+ // if it fails due to EOF, still allow user to type in XTerm
317
+ setAwaitingInput(true);
318
  } finally {
319
  setIsRunning(false);
320
  }
321
  };
322
 
323
+ // Legacy send from input box
324
  const sendTerminalInput = async () => {
325
+ const node = getNodeByPath(tree, activePath);
326
+ if (!node || node.type !== "file") {
327
  setOutput((prev) => prev + "\nNo file selected.");
328
  setAwaitingInput(false);
329
  return;
 
336
  setTerminalInput("");
337
  setIsRunning(true);
338
  try {
339
+ const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
340
+ const res = await runCode(node.content, selectedLang, newAccum);
341
  const out = res.output ?? "";
342
  setOutput(out);
343
  setProblems(res.error ? parseProblems(res.output) : []);
 
354
  }
355
  };
356
 
 
357
  const onTerminalKeyDown = (e) => {
358
  if (e.key === "Enter") {
359
  e.preventDefault();
 
410
  }
411
  };
412
 
413
+ // AI suggestions for continuation
414
  const fetchAiSuggestions = async (code) => {
415
  if (!code?.trim()) return;
416
  try {
417
  const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
418
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
419
  } catch {
420
+ // ignore
421
  }
422
  };
423
 
 
430
  alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
431
  };
432
 
433
+ // Editor change
434
  const updateActiveFileContent = (value) => {
435
  const node = getNodeByPath(tree, activePath);
436
  if (!node) return;
 
438
  setTree(updated);
439
  };
440
 
441
+ // Render tree
442
  const renderTree = (node, depth = 0) => {
443
  const isActive = node.path === activePath;
444
  return (
 
462
  );
463
  };
464
 
 
465
  const anyLoading = isRunning || isFixing || isExplaining;
466
 
467
+ // JSX
468
  return (
469
  <div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}>
 
470
  <input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
471
 
 
472
  <div className="ide-menubar">
473
  <div className="ide-menubar-left">
474
  <span className="ide-logo">⚙️ DevMate IDE</span>
 
493
  </div>
494
  </div>
495
 
 
496
  {anyLoading && (
497
  <div className="ide-progress-wrap">
498
  <div className="ide-progress" />
499
  </div>
500
  )}
501
 
 
502
  <div className="ide-body">
 
503
  <div className="ide-sidebar">
504
  <div className="ide-sidebar-header">
505
  <span>EXPLORER</span>
 
508
  <div className="ide-file-list" style={{ padding: 6 }}>{renderTree(tree)}</div>
509
  </div>
510
 
 
511
  <div className="ide-main">
512
  <div className="ide-editor-wrapper">
513
  <Editor
 
522
  />
523
  </div>
524
 
 
525
  {aiSuggestions.length > 0 && (
526
  <div className="ai-popup">
527
  {aiSuggestions.map((s, i) => (
528
+ <div key={i} className="ai-suggest" onClick={() => updateFileContent(tree, activePath, (currentNode?.content || "") + "\n" + s)}>{s}</div>
529
  ))}
530
  </div>
531
  )}
532
 
 
533
  <div className="ide-panels">
 
534
  <div style={{ marginBottom: 8 }}>
535
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
536
 
 
 
537
  <XTerm
538
  output={output}
539
  onData={(line) => {
 
540
  runCodeWithUpdatedInput(line);
541
  }}
542
  />
543
 
 
544
  {!awaitingInput && (
545
  <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
546
  <input className="ide-input-box" placeholder="(Optional) Program input for single-run" value={stdin} onChange={(e) => setStdin(e.target.value)} />
 
548
  </div>
549
  )}
550
 
 
551
  {awaitingInput && (
552
  <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
553
  <input
 
563
  )}
564
  </div>
565
 
 
566
  {problems.length > 0 && (
567
  <div className="ide-problems-panel">
568
  <div>🚨 Problems ({problems.length})</div>
 
572
  </div>
573
  </div>
574
 
 
575
  <div className="ide-right-panel">
576
  <div className="ide-ai-header">🤖 AI Assistant</div>
577
 
 
594
  </div>
595
  </div>
596
 
 
597
  {searchOpen && (
598
  <div className="search-dialog">
599
  <input placeholder="Search text..." onChange={(e) => setSearchQuery(e.target.value)} />
 
601
  </div>
602
  )}
603
 
 
604
  {contextMenu && (
605
  <div className="ide-context-menu" style={{ top: contextMenu.y, left: contextMenu.x }} onMouseLeave={() => setContextMenu(null)}>
606
  <div onClick={() => { setContextMenu(null); handleRename(); }}>✏️ Rename</div>