FrederickSundeep commited on
Commit
34104e6
·
1 Parent(s): 950fad2

commit initial 09-12-2025 07

Browse files
Files changed (1) hide show
  1. src/App.js +164 -127
src/App.js CHANGED
@@ -16,6 +16,8 @@ import {
16
  import { downloadProjectZip } from "./zipExport";
17
  import { parseProblems } from "./problemParser";
18
  import "./App.css";
 
 
19
 
20
  // =================== SUPPORTED LANGUAGES ===================
21
  const LANGUAGE_OPTIONS = [
@@ -50,20 +52,40 @@ function outputLooksForInput(output) {
50
  return patterns.some((p) => p.test(o));
51
  }
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  // =================== APP ===================
54
  function App() {
55
  const [tree, setTree] = useState(loadTree());
56
  const [activePath, setActivePath] = useState("main.py"); // selected file or folder path
 
57
  // Terminal-ish state
58
- const [terminalLines, setTerminalLines] = useState([]); // array of strings shown in terminal
59
- const [terminalInput, setTerminalInput] = useState(""); // current typed input in terminal prompt
60
  const [accumStdin, setAccumStdin] = useState(""); // accumulated stdin passed to backend
61
  const [awaitingInput, setAwaitingInput] = useState(false); // true if program waiting for input
62
- const [output, setOutput] = useState(""); // last raw output (kept for compatibility)
63
  const [prompt, setPrompt] = useState("");
64
  const [explanation, setExplanation] = useState("");
65
- // other states
66
- const [stdin, setStdin] = useState(""); // legacy single-run input (kept for compatibility)
 
 
67
  const [problems, setProblems] = useState([]);
68
  const [theme, setTheme] = useState("vs-dark");
69
  const [searchOpen, setSearchOpen] = useState(false);
@@ -200,94 +222,108 @@ function App() {
200
  e.target.value = "";
201
  };
202
 
203
- // ---------- Terminal/Run logic (interactive) ----------
204
- // We will accumulate stdin in `accumStdin`. On initial run we clear it.
205
- // When backend returns output containing cues that input is requested, set awaitingInput=true.
206
- // When user types into terminal prompt and presses Enter, append that input to accumStdin + "\n",
207
- // then re-run the program with updated accumStdin. Repeat until no input cues.
208
-
209
- const resetTerminal = (keepAccum = false) => {
210
- setTerminalLines([]);
211
- setTerminalInput("");
212
- // only reset accumulated stdin if caller explicitly wants to clear it
213
- if (!keepAccum) {
214
- setAccumStdin("");
215
- }
216
- setAwaitingInput(false);
217
- };
218
-
219
- const appendTerminal = (text) => {
220
- setTerminalLines((prev) => [...prev, text]);
221
  };
222
 
223
- // helper: detect input calls in source (python/java/js)
224
- function codeNeedsInput(code, langId) {
225
- if (!code) return false;
226
- try {
227
- const c = code.toString();
228
- if (langId === "python") {
229
- return /\binput\s*\(/i.test(c);
 
 
 
 
 
 
230
  }
231
- if (langId === "java") {
232
- return /\bScanner\s*\(|\bnext(Int|Line|Double|())\b/i.test(c) || /\bSystem\.console\(\)/i.test(c);
 
 
 
 
233
  }
234
- if (langId === "javascript") {
235
- // node.js stdin reads or prompt usage heuristics
236
- return /process\.stdin|readline|prompt\(|window\.prompt/i.test(c);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  }
238
- // fallback: look for common words
239
- return /\binput\b|\bScanner\b|\bnextInt\b|prompt\(/i.test(c);
240
- } catch {
241
- return false;
242
- }
243
- }
244
 
 
245
  const handleRun = async () => {
246
- const node = getNodeByPath(tree, activePath);
247
- if (!node || node.type !== "file") {
248
- setOutput("Select a file to run.");
249
- return;
250
- }
251
- const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
252
- if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
253
- setOutput(`⚠️ Run not supported for this file type.`);
254
- return;
255
- }
256
-
257
- // If code likely needs input and accumStdin is empty, ask for initial input
258
- const needs = codeNeedsInput(node.content, selectedLang);
259
- if (needs && !accumStdin) {
260
- // collect multi-line input: user can paste multiple lines separated by \n
261
- const userText = window.prompt(
262
- "This program appears to request console input (e.g. input() / Scanner).\n\nEnter input lines separated by a newline (\\n). Leave empty to run without input.",
263
- ""
264
- );
265
- if (userText == null) {
266
- // user pressed cancel: proceed but warn
267
- appendTerminal("[Run cancelled by user]");
268
  return;
269
  }
270
- // userText may contain literal backslash-n if pasted; keep it as-is.
271
- // Normalize: if the user typed using Enter, it's already multi-line.
272
- const prepared = userText.replace(/\\n/g, "\n");
273
- setAccumStdin(prepared);
274
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
- // clear terminal state and start a fresh run
277
- resetTerminal();
278
  setIsRunning(true);
279
  setProblems([]);
280
- setOutput("");
281
  try {
282
- // call backend with current accumStdin (empty on first run)
283
- const res = await runCode(node.content, selectedLang, accumStdin || stdin || "");
284
  const out = res.output ?? "";
285
  setOutput(out);
286
- appendTerminal(out);
287
  setProblems(res.error ? parseProblems(res.output) : []);
288
-
289
  if (outputLooksForInput(out)) {
290
- // program likely wants input: show prompt
291
  setAwaitingInput(true);
292
  } else {
293
  setAwaitingInput(false);
@@ -295,53 +331,46 @@ function codeNeedsInput(code, langId) {
295
  } catch (err) {
296
  const e = String(err);
297
  setOutput(e);
298
- appendTerminal(e);
299
  setAwaitingInput(false);
300
  } finally {
301
  setIsRunning(false);
302
  }
303
  };
304
 
305
- // Called when user types input into terminal and presses Enter
306
  const sendTerminalInput = async () => {
307
- if (!currentNode || currentNode.type !== "file") {
308
- appendTerminal("No file selected.");
309
- setAwaitingInput(false);
310
- return;
311
- }
312
-
313
- // If not already in awaitingInput (heuristic missed), still allow sending input:
314
- if (!awaitingInput && !isRunning && terminalInput.trim()) {
315
- appendTerminal("[Info] Sending input to program (re-run).");
316
- }
317
 
318
- const userText = terminalInput;
319
- appendTerminal(`> ${userText}`);
320
- const newAccum = (accumStdin || "") + userText + "\n";
321
- setAccumStdin(newAccum);
322
- setTerminalInput("");
323
- setIsRunning(true);
324
- try {
325
- const selectedLang = LANGUAGE_OPTIONS.find((l) => currentNode.name.endsWith(l.ext))?.id;
326
- const res = await runCode(currentNode.content, selectedLang, newAccum);
327
- const out = res.output ?? "";
328
- setOutput(out);
329
- appendTerminal(out);
330
- setProblems(res.error ? parseProblems(res.output) : []);
331
- if (outputLooksForInput(out)) {
332
- setAwaitingInput(true);
333
- } else {
 
 
 
334
  setAwaitingInput(false);
 
 
335
  }
336
- } catch (err) {
337
- appendTerminal(String(err));
338
- setAwaitingInput(false);
339
- } finally {
340
- setIsRunning(false);
341
- }
342
- };
343
 
344
- // Allow pressing Enter to send input
345
  const onTerminalKeyDown = (e) => {
346
  if (e.key === "Enter") {
347
  e.preventDefault();
@@ -528,20 +557,34 @@ function codeNeedsInput(code, langId) {
528
 
529
  {/* Bottom panels */}
530
  <div className="ide-panels">
531
- {/* Terminal output */}
532
- <div className="ide-output-terminal" style={{ marginBottom: 8 }}>
533
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
534
- <div className="terminal-content" style={{ background: "#000", color: "#0f0", padding: 8, borderRadius: 6, maxHeight: 220, overflowY: "auto", whiteSpace: "pre-wrap", fontFamily: "Consolas, monospace" }}>
535
- {terminalLines.length === 0 ? <div style={{ color: "#999" }}>[Program output will appear here]</div> : terminalLines.map((ln, i) => <div key={i}>{ln}</div>)}
536
- {awaitingInput && <div style={{ color: "#fff" }}>&nbsp;</div>}
537
- </div>
538
 
539
- {/* Terminal input (shown only when program asks input) */}
540
- {awaitingInput ? (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
542
  <input
543
  className="ide-input-box"
544
- placeholder="Type input and press Enter..."
545
  value={terminalInput}
546
  onChange={(e) => setTerminalInput(e.target.value)}
547
  onKeyDown={onTerminalKeyDown}
@@ -549,12 +592,6 @@ function codeNeedsInput(code, langId) {
549
  />
550
  <button onClick={sendTerminalInput} disabled={!awaitingInput || isRunning} className="ide-button">Send</button>
551
  </div>
552
- ) : (
553
- // If not awaiting input, show legacy single-run input + hint to run for interactive programs
554
- <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
555
- <input className="ide-input-box" placeholder="(Optional) Program input for single-run" value={stdin} onChange={(e) => setStdin(e.target.value)} />
556
- <div style={{ alignSelf: "center", color: "#999", fontSize: 12 }}>Press Run → to execute</div>
557
- </div>
558
  )}
559
  </div>
560
 
 
16
  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 = [
 
52
  return patterns.some((p) => p.test(o));
53
  }
54
 
55
+ function codeNeedsInput(code, langId) {
56
+ if (!code) return false;
57
+ try {
58
+ const c = code.toString();
59
+ if (langId === "python") {
60
+ return /\binput\s*\(/i.test(c);
61
+ }
62
+ if (langId === "java") {
63
+ return /\bScanner\s*\(|\bnext(Int|Line|Double)\b/i.test(c) || /\bSystem\.console\(\)/i.test(c);
64
+ }
65
+ if (langId === "javascript") {
66
+ return /process\.stdin|readline|prompt\(|window\.prompt/i.test(c);
67
+ }
68
+ return /\binput\b|\bScanner\b|\bnextInt\b|prompt\(/i.test(c);
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
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);
 
222
  e.target.value = "";
223
  };
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
+ }
233
+ setAwaitingInput(false);
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;
250
  }
251
+
252
+ const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
253
+ if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
254
+ setOutput((prev) => prev + `\n[Error] Run not supported for ${node.name}`);
255
+ setAwaitingInput(false);
256
+ return;
257
  }
258
+
259
+ setIsRunning(true);
260
+ try {
261
+ const res = await runCode(node.content, selectedLang, newAccum);
262
+ const out = res.output ?? "";
263
+ setOutput(out);
264
+ setProblems(res.error ? parseProblems(res.output) : []);
265
+ if (outputLooksForInput(out)) {
266
+ setAwaitingInput(true);
267
+ } else {
268
+ setAwaitingInput(false);
269
+ }
270
+ } catch (err) {
271
+ setOutput(String(err));
272
+ setAwaitingInput(false);
273
+ } finally {
274
+ setIsRunning(false);
275
  }
276
+ };
 
 
 
 
 
277
 
278
+ // ---------- Run (initial) ----------
279
  const handleRun = async () => {
280
+ const node = getNodeByPath(tree, activePath);
281
+ if (!node || node.type !== "file") {
282
+ setOutput("Select a file to run.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  return;
284
  }
285
+ const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
286
+ if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
287
+ setOutput(`⚠️ Run not supported for this file type.`);
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);
 
325
  setProblems(res.error ? parseProblems(res.output) : []);
 
326
  if (outputLooksForInput(out)) {
 
327
  setAwaitingInput(true);
328
  } else {
329
  setAwaitingInput(false);
 
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;
346
+ }
 
 
 
 
 
347
 
348
+ const userText = terminalInput;
349
+ setOutput((prev) => prev + `\n> ${userText}`);
350
+ const newAccum = (accumStdin || "") + userText + "\n";
351
+ setAccumStdin(newAccum);
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) : []);
360
+ if (outputLooksForInput(out)) {
361
+ setAwaitingInput(true);
362
+ } else {
363
+ setAwaitingInput(false);
364
+ }
365
+ } catch (err) {
366
+ setOutput((prev) => prev + "\n" + String(err));
367
  setAwaitingInput(false);
368
+ } finally {
369
+ setIsRunning(false);
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();
 
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)} />
578
+ <div style={{ alignSelf: "center", color: "#999", fontSize: 12 }}>Press Run → to execute</div>
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
586
  className="ide-input-box"
587
+ placeholder="Type input and press Send..."
588
  value={terminalInput}
589
  onChange={(e) => setTerminalInput(e.target.value)}
590
  onKeyDown={onTerminalKeyDown}
 
592
  />
593
  <button onClick={sendTerminalInput} disabled={!awaitingInput || isRunning} className="ide-button">Send</button>
594
  </div>
 
 
 
 
 
 
595
  )}
596
  </div>
597