Spaces:
Running
Running
Commit Β·
921b162
1
Parent(s): eb69de5
commit initial 09-12-2025 018
Browse files- src/App.js +145 -33
src/App.js
CHANGED
|
@@ -32,14 +32,38 @@ const LANGUAGE_OPTIONS = [
|
|
| 32 |
|
| 33 |
const RUNNABLE_LANGS = ["python", "javascript", "java"];
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
// =================== APP ===================
|
| 36 |
function App() {
|
| 37 |
const [tree, setTree] = useState(loadTree());
|
| 38 |
const [activePath, setActivePath] = useState("main.py"); // selected file or folder path
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
const [prompt, setPrompt] = useState("");
|
| 41 |
const [explanation, setExplanation] = useState("");
|
| 42 |
-
|
|
|
|
| 43 |
const [problems, setProblems] = useState([]);
|
| 44 |
const [theme, setTheme] = useState("vs-dark");
|
| 45 |
const [searchOpen, setSearchOpen] = useState(false);
|
|
@@ -65,7 +89,7 @@ function App() {
|
|
| 65 |
LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
|
| 66 |
LANGUAGE_OPTIONS[0];
|
| 67 |
|
| 68 |
-
// ----------
|
| 69 |
const collectFolderPaths = (node, acc = []) => {
|
| 70 |
if (!node) return acc;
|
| 71 |
if (node.type === "folder") acc.push(node.path || "");
|
|
@@ -176,7 +200,23 @@ function App() {
|
|
| 176 |
e.target.value = "";
|
| 177 |
};
|
| 178 |
|
| 179 |
-
// ---------- Run
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
const handleRun = async () => {
|
| 181 |
const node = getNodeByPath(tree, activePath);
|
| 182 |
if (!node || node.type !== "file") {
|
|
@@ -189,20 +229,83 @@ function App() {
|
|
| 189 |
return;
|
| 190 |
}
|
| 191 |
|
|
|
|
|
|
|
| 192 |
setIsRunning(true);
|
| 193 |
-
setOutput(""); // clear previous
|
| 194 |
setProblems([]);
|
|
|
|
| 195 |
try {
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
setProblems(res.error ? parseProblems(res.output) : []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
} catch (err) {
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
| 201 |
} finally {
|
| 202 |
setIsRunning(false);
|
| 203 |
}
|
| 204 |
};
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
const handleAskFix = async () => {
|
| 207 |
const node = getNodeByPath(tree, activePath);
|
| 208 |
if (!node || node.type !== "file") {
|
|
@@ -327,19 +430,9 @@ function App() {
|
|
| 327 |
|
| 328 |
<div className="ide-menubar-right">
|
| 329 |
<button onClick={handleSearchToggle} disabled={anyLoading}>π Search</button>
|
| 330 |
-
|
| 331 |
-
<button onClick={
|
| 332 |
-
|
| 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>
|
|
@@ -375,11 +468,7 @@ function App() {
|
|
| 375 |
onChange={updateActiveFileContent}
|
| 376 |
onMount={(editor) => (editorRef.current = editor)}
|
| 377 |
onBlur={() => fetchAiSuggestions(currentNode?.content)}
|
| 378 |
-
options={{
|
| 379 |
-
minimap: { enabled: true },
|
| 380 |
-
fontSize: 14,
|
| 381 |
-
scrollBeyondLastLine: false,
|
| 382 |
-
}}
|
| 383 |
/>
|
| 384 |
</div>
|
| 385 |
|
|
@@ -394,18 +483,41 @@ function App() {
|
|
| 394 |
|
| 395 |
{/* Bottom panels */}
|
| 396 |
<div className="ide-panels">
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
|
|
|
| 401 |
{problems.length > 0 && (
|
| 402 |
<div className="ide-problems-panel">
|
| 403 |
<div>π¨ Problems ({problems.length})</div>
|
| 404 |
-
{problems.map((p, i) =>
|
| 405 |
-
<div key={i}>
|
| 406 |
-
{p.path}:{p.line} β {p.message}
|
| 407 |
-
</div>
|
| 408 |
-
))}
|
| 409 |
</div>
|
| 410 |
)}
|
| 411 |
</div>
|
|
|
|
| 32 |
|
| 33 |
const RUNNABLE_LANGS = ["python", "javascript", "java"];
|
| 34 |
|
| 35 |
+
// Utility: detect whether output likely requests input
|
| 36 |
+
function outputLooksForInput(output) {
|
| 37 |
+
if (!output) return false;
|
| 38 |
+
const o = output.toString();
|
| 39 |
+
// common prompt words / patterns and trailing prompt char
|
| 40 |
+
const patterns = [
|
| 41 |
+
/enter.*:/i,
|
| 42 |
+
/input.*:/i,
|
| 43 |
+
/please enter/i,
|
| 44 |
+
/scanner/i, // java Scanner exceptions or prompts
|
| 45 |
+
/press enter/i,
|
| 46 |
+
/: $/, // ends with colon + space (e.g. "Enter number: ")
|
| 47 |
+
/:\n$/, // ends with colon + newline
|
| 48 |
+
/> $/, // trailing >
|
| 49 |
+
];
|
| 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);
|
|
|
|
| 89 |
LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
|
| 90 |
LANGUAGE_OPTIONS[0];
|
| 91 |
|
| 92 |
+
// ---------- TREE helpers ----------
|
| 93 |
const collectFolderPaths = (node, acc = []) => {
|
| 94 |
if (!node) return acc;
|
| 95 |
if (node.type === "folder") acc.push(node.path || "");
|
|
|
|
| 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 = () => {
|
| 210 |
+
setTerminalLines([]);
|
| 211 |
+
setTerminalInput("");
|
| 212 |
+
setAccumStdin("");
|
| 213 |
+
setAwaitingInput(false);
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
const appendTerminal = (text) => {
|
| 217 |
+
setTerminalLines((prev) => [...prev, text]);
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
const handleRun = async () => {
|
| 221 |
const node = getNodeByPath(tree, activePath);
|
| 222 |
if (!node || node.type !== "file") {
|
|
|
|
| 229 |
return;
|
| 230 |
}
|
| 231 |
|
| 232 |
+
// clear terminal state and start a fresh run
|
| 233 |
+
resetTerminal();
|
| 234 |
setIsRunning(true);
|
|
|
|
| 235 |
setProblems([]);
|
| 236 |
+
setOutput("");
|
| 237 |
try {
|
| 238 |
+
// call backend with current accumStdin (empty on first run)
|
| 239 |
+
const res = await runCode(node.content, selectedLang, accumStdin || stdin || "");
|
| 240 |
+
const out = res.output ?? "";
|
| 241 |
+
setOutput(out);
|
| 242 |
+
appendTerminal(out);
|
| 243 |
setProblems(res.error ? parseProblems(res.output) : []);
|
| 244 |
+
|
| 245 |
+
if (outputLooksForInput(out)) {
|
| 246 |
+
// program likely wants input: show prompt
|
| 247 |
+
setAwaitingInput(true);
|
| 248 |
+
} else {
|
| 249 |
+
setAwaitingInput(false);
|
| 250 |
+
}
|
| 251 |
} catch (err) {
|
| 252 |
+
const e = String(err);
|
| 253 |
+
setOutput(e);
|
| 254 |
+
appendTerminal(e);
|
| 255 |
+
setAwaitingInput(false);
|
| 256 |
} finally {
|
| 257 |
setIsRunning(false);
|
| 258 |
}
|
| 259 |
};
|
| 260 |
|
| 261 |
+
// Called when user types input into terminal and presses Enter
|
| 262 |
+
const sendTerminalInput = async () => {
|
| 263 |
+
if (!awaitingInput) return;
|
| 264 |
+
const userText = terminalInput;
|
| 265 |
+
// echo user input in terminal
|
| 266 |
+
appendTerminal(`> ${userText}`);
|
| 267 |
+
const newAccum = (accumStdin || "") + userText + "\n";
|
| 268 |
+
setAccumStdin(newAccum);
|
| 269 |
+
setTerminalInput("");
|
| 270 |
+
setIsRunning(true);
|
| 271 |
+
try {
|
| 272 |
+
// re-run program with updated stdin
|
| 273 |
+
const node = getNodeByPath(tree, activePath);
|
| 274 |
+
if (!node || node.type !== "file") {
|
| 275 |
+
appendTerminal("No file selected.");
|
| 276 |
+
setAwaitingInput(false);
|
| 277 |
+
return;
|
| 278 |
+
}
|
| 279 |
+
const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
|
| 280 |
+
const res = await runCode(node.content, selectedLang, newAccum);
|
| 281 |
+
const out = res.output ?? "";
|
| 282 |
+
setOutput(out);
|
| 283 |
+
appendTerminal(out);
|
| 284 |
+
setProblems(res.error ? parseProblems(res.output) : []);
|
| 285 |
+
// detect if program still waiting
|
| 286 |
+
if (outputLooksForInput(out)) {
|
| 287 |
+
setAwaitingInput(true);
|
| 288 |
+
} else {
|
| 289 |
+
setAwaitingInput(false);
|
| 290 |
+
}
|
| 291 |
+
} catch (err) {
|
| 292 |
+
appendTerminal(String(err));
|
| 293 |
+
setAwaitingInput(false);
|
| 294 |
+
} finally {
|
| 295 |
+
setIsRunning(false);
|
| 296 |
+
}
|
| 297 |
+
};
|
| 298 |
+
|
| 299 |
+
// Allow pressing Enter to send input
|
| 300 |
+
const onTerminalKeyDown = (e) => {
|
| 301 |
+
if (e.key === "Enter") {
|
| 302 |
+
e.preventDefault();
|
| 303 |
+
if (!awaitingInput) return;
|
| 304 |
+
sendTerminalInput();
|
| 305 |
+
}
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
// ---------- Agent functions ----------
|
| 309 |
const handleAskFix = async () => {
|
| 310 |
const node = getNodeByPath(tree, activePath);
|
| 311 |
if (!node || node.type !== "file") {
|
|
|
|
| 430 |
|
| 431 |
<div className="ide-menubar-right">
|
| 432 |
<button onClick={handleSearchToggle} disabled={anyLoading}>π Search</button>
|
| 433 |
+
<button onClick={handleRun} disabled={isRunning || anyLoading}>{isRunning ? "β³ Running..." : "βΆ Run"}</button>
|
| 434 |
+
<button onClick={handleAskFix} disabled={isFixing || anyLoading}>{isFixing ? "β³ Fixing..." : "π€ Fix"}</button>
|
| 435 |
+
<button onClick={handleExplainSelection} disabled={isExplaining || anyLoading}>{isExplaining ? "β³ Explaining..." : "π Explain"}</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
<button onClick={() => setTheme((t) => (t === "vs-dark" ? "light" : "vs-dark"))} disabled={anyLoading}>
|
| 437 |
{theme === "vs-dark" ? "βοΈ" : "π"}
|
| 438 |
</button>
|
|
|
|
| 468 |
onChange={updateActiveFileContent}
|
| 469 |
onMount={(editor) => (editorRef.current = editor)}
|
| 470 |
onBlur={() => fetchAiSuggestions(currentNode?.content)}
|
| 471 |
+
options={{ minimap: { enabled: true }, fontSize: 14, scrollBeyondLastLine: false }}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
/>
|
| 473 |
</div>
|
| 474 |
|
|
|
|
| 483 |
|
| 484 |
{/* Bottom panels */}
|
| 485 |
<div className="ide-panels">
|
| 486 |
+
{/* Terminal output */}
|
| 487 |
+
<div className="ide-output-terminal" style={{ marginBottom: 8 }}>
|
| 488 |
+
<div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
|
| 489 |
+
<div className="terminal-content" style={{ background: "#000", color: "#0f0", padding: 8, borderRadius: 6, maxHeight: 220, overflowY: "auto", whiteSpace: "pre-wrap", fontFamily: "Consolas, monospace" }}>
|
| 490 |
+
{terminalLines.length === 0 ? <div style={{ color: "#999" }}>[Program output will appear here]</div> : terminalLines.map((ln, i) => <div key={i}>{ln}</div>)}
|
| 491 |
+
{awaitingInput && <div style={{ color: "#fff" }}> </div>}
|
| 492 |
+
</div>
|
| 493 |
|
| 494 |
+
{/* Terminal input (shown only when program asks input) */}
|
| 495 |
+
{awaitingInput ? (
|
| 496 |
+
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
|
| 497 |
+
<input
|
| 498 |
+
className="ide-input-box"
|
| 499 |
+
placeholder="Type input and press Enter..."
|
| 500 |
+
value={terminalInput}
|
| 501 |
+
onChange={(e) => setTerminalInput(e.target.value)}
|
| 502 |
+
onKeyDown={onTerminalKeyDown}
|
| 503 |
+
disabled={!awaitingInput || isRunning}
|
| 504 |
+
/>
|
| 505 |
+
<button onClick={sendTerminalInput} disabled={!awaitingInput || isRunning} className="ide-button">Send</button>
|
| 506 |
+
</div>
|
| 507 |
+
) : (
|
| 508 |
+
// If not awaiting input, show legacy single-run input + hint to run for interactive programs
|
| 509 |
+
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
|
| 510 |
+
<input className="ide-input-box" placeholder="(Optional) Program input for single-run" value={stdin} onChange={(e) => setStdin(e.target.value)} />
|
| 511 |
+
<div style={{ alignSelf: "center", color: "#999", fontSize: 12 }}>Press Run β to execute</div>
|
| 512 |
+
</div>
|
| 513 |
+
)}
|
| 514 |
+
</div>
|
| 515 |
|
| 516 |
+
{/* Problems */}
|
| 517 |
{problems.length > 0 && (
|
| 518 |
<div className="ide-problems-panel">
|
| 519 |
<div>π¨ Problems ({problems.length})</div>
|
| 520 |
+
{problems.map((p, i) => <div key={i}>{p.path}:{p.line} β {p.message}</div>)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
</div>
|
| 522 |
)}
|
| 523 |
</div>
|