working socket integration

This commit is contained in:
2025-04-21 21:03:59 +05:30
parent c143efa70e
commit 4453e69e68
22 changed files with 2070 additions and 60 deletions

View File

@@ -8,6 +8,7 @@ import {
} from "lucide-react";
import Sidebar from "./Sidebar";
import Panel from "./Panel"; // Import Panel component
import WebSocketTerminal from "./WebSocketTerminal"; // Import WebSocket Terminal
const EditorArea = ({
sidebarVisible = true,
@@ -64,6 +65,10 @@ const EditorArea = ({
const [userInput, setUserInput] = useState("");
// Add a new state for waiting for input
const [waitingForInput, setWaitingForInput] = useState(false);
// Add a new state for tracking the active submission ID
const [activeRunningSubmissionId, setActiveRunningSubmissionId] = useState(null);
// Add a state to toggle between regular and WebSocket terminals
const [useWebSocket, setUseWebSocket] = useState(false);
// Focus the input when new file modal opens
useEffect(() => {
@@ -507,7 +512,7 @@ Happy coding!`;
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
};
// Modify the handleRunCode function to prompt for input first
// Modified handleRunCode to start execution immediately
const handleRunCode = async () => {
if (!activeFile) return;
@@ -517,58 +522,47 @@ Happy coding!`;
setPanelVisible(true);
}
// Set state to waiting for input
setWaitingForInput(true);
// Reset states
setIsRunning(true);
setWaitingForInput(false);
setActiveRunningFile(activeFile.id);
setActiveRunningSubmissionId(null);
setUserInput('');
// Clear previous output and add new command
// Get language from file extension
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
const language = getLanguageFromExtension(fileExtension);
// If using WebSocket mode, we'll use the WebSocketTerminal component
if (useWebSocket) {
// Just set the running state, the WebSocketTerminal will handle the rest
return;
}
// Regular HTTP mode - use polling
// Clear previous output and add new command
const newOutput = [
{ type: 'command', content: `$ run ${activeFile.id}` },
{ type: 'output', content: '------- PROGRAM EXECUTION -------' },
{ type: 'output', content: `Language: ${language}` },
{ type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' }
{ type: 'output', content: 'Executing code...' }
];
setTerminalOutput(newOutput);
};
// Add a new function to handle input submission
const handleInputSubmit = async () => {
if (!activeFile || !waitingForInput) return;
// Set running state
setIsRunning(true);
setWaitingForInput(false);
// Add message that we're running with the input
if (userInput) {
setTerminalOutput(prev => [
...prev,
{ type: 'input', content: userInput }
]);
} else {
setTerminalOutput(prev => [
...prev,
{ type: 'output', content: 'Running without input...' }
]);
}
// Use API URL from environment variable
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
try {
// Now make the API call with the input that was entered
// Submit the code for execution immediately
const submitResponse = await fetch(`${apiUrl}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
language: getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase()),
language: getLanguageFromExtension(fileExtension),
code: activeFile.content,
input: userInput
input: '' // No initial input
}),
});
@@ -577,11 +571,81 @@ Happy coding!`;
}
const { id } = await submitResponse.json();
setActiveRunningSubmissionId(id);
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
// Step 2: Poll for status until completed or failed
// Start polling for status and output
pollForStatusAndOutput(id);
} catch (error) {
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
setIsRunning(false);
setActiveRunningSubmissionId(null);
}
};
// Toggle between WebSocket and HTTP modes
const toggleWebSocketMode = () => {
setUseWebSocket(!useWebSocket);
};
// Simplified handleInputSubmit to only handle interactive input
const handleInputSubmit = async () => {
if (!waitingForInput || !activeRunningSubmissionId) return;
// Store the input value before clearing it
const inputValue = userInput;
// Clear the input field and reset waiting state immediately for better UX
setUserInput('');
setWaitingForInput(false);
// Add the input to the terminal immediately
setTerminalOutput(prev => [
...prev,
{ type: 'input', content: inputValue }
]);
// Use API URL from environment variable
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
try {
// Submit input to the running program
const submitInputResponse = await fetch(`${apiUrl}/submit-input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: activeRunningSubmissionId,
input: inputValue
}),
});
if (!submitInputResponse.ok) {
throw new Error(`Server error: ${submitInputResponse.status}`);
}
// Wait for a moment to allow the program to process the input
await new Promise(resolve => setTimeout(resolve, 500));
// Poll for status and check if we need more input
pollForStatusAndOutput(activeRunningSubmissionId);
} catch (error) {
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
setIsRunning(false);
setActiveRunningSubmissionId(null);
}
};
// Add a function to poll for status and output
const pollForStatusAndOutput = async (id) => {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
try {
// Step 2: Poll for status until completed, failed, or waiting_for_input
let status = 'pending';
while (status !== 'completed' && status !== 'failed') {
while (status !== 'completed' && status !== 'failed' && status !== 'waiting_for_input') {
// Add a small delay between polls
await new Promise(resolve => setTimeout(resolve, 1000));
@@ -609,6 +673,90 @@ Happy coding!`;
});
}
// Check if we're waiting for input
if (status === 'waiting_for_input') {
// Get the current output to display to the user
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
if (resultResponse.ok) {
const { output } = await resultResponse.json();
// Process the output to show what's happened so far
const outputLines = [];
let promptText = '';
// Split by lines and process each line
const lines = output.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('[Input] ')) {
// This is an input line
outputLines.push({
type: 'input',
content: line.substring(8) // Remove the '[Input] ' prefix
});
} else if (line === '[WAITING_FOR_INPUT]') {
// This is a marker for waiting for input
// If there's a line before this, it's likely the prompt
if (i > 0 && lines[i-1].trim() !== '') {
promptText = lines[i-1];
}
continue;
} else if (line.trim() !== '') {
// This is a regular output line
outputLines.push({
type: 'output',
content: line
});
}
}
// Update the terminal with the current output
if (outputLines.length > 0) {
setTerminalOutput(prev => {
// Keep only the essential lines to avoid duplication
const essentialLines = prev.filter(line =>
line.type === 'command' ||
line.content.includes('PROGRAM EXECUTION') ||
line.content.includes('Language:') ||
line.content.includes('Job submitted') ||
line.content.includes('Status:') ||
line.content === 'Executing code...'
);
return [...essentialLines, ...outputLines];
});
}
// Now set the waiting for input state
setWaitingForInput(true);
// Add a message indicating we're waiting for input
setTerminalOutput(prev => {
// Remove any existing waiting message
const filteredPrev = prev.filter(line =>
line.content !== 'Waiting for input...'
);
// Add the prompt text if available
if (promptText) {
return [...filteredPrev, {
type: 'prompt',
content: promptText
}, {
type: 'output',
content: 'Waiting for input...'
}];
} else {
return [...filteredPrev, {
type: 'output',
content: 'Waiting for input...'
}];
}
});
}
return;
}
// Get the result for both completed and failed status
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
if (!resultResponse.ok) {
@@ -653,11 +801,16 @@ Happy coding!`;
console.error('Code execution failed:', output);
}
// Reset state
setIsRunning(false);
setWaitingForInput(false);
setActiveRunningSubmissionId(null);
} catch (error) {
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
} finally {
// Set running state to false
setIsRunning(false);
setWaitingForInput(false);
setActiveRunningSubmissionId(null);
}
};
@@ -791,15 +944,38 @@ Happy coding!`;
title="Run code"
>
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
</button>
<button
className="terminal-toggle-button"
onClick={togglePanel} // Use the new function
onClick={togglePanel}
title="Toggle terminal"
>
<Terminal size={16} />
</button>
<button
className={`websocket-toggle-button ${useWebSocket ? 'active' : ''}`}
onClick={toggleWebSocketMode}
title={`${useWebSocket ? 'Disable' : 'Enable'} WebSocket mode`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12s2-3 5-3 5 3 5 3-2 3-5 3-5-3-5-3z"></path>
<circle cx="12" cy="12" r="1"></circle>
<path d="M2 4l3 3"></path>
<path d="M22 4l-3 3"></path>
<path d="M2 20l3-3"></path>
<path d="M22 20l-3-3"></path>
</svg>
</button>
</>
)}
</div>
@@ -858,18 +1034,28 @@ Happy coding!`;
document.addEventListener("mouseup", onMouseUp);
}}
/>
<Panel
height={panelHeight}
terminalOutput={terminalOutput}
isRunning={isRunning}
waitingForInput={waitingForInput}
activeRunningFile={activeRunningFile}
initialTab="terminal"
onClose={togglePanel}
userInput={userInput}
onUserInputChange={setUserInput}
onInputSubmit={handleInputSubmit}
/>
{useWebSocket && activeFile ? (
<div style={{ height: panelHeight + 'px' }}>
<WebSocketTerminal
code={activeFile.content}
language={getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase())}
onClose={togglePanel}
/>
</div>
) : (
<Panel
height={panelHeight}
terminalOutput={terminalOutput}
isRunning={isRunning}
waitingForInput={waitingForInput}
activeRunningFile={activeRunningFile}
initialTab="terminal"
onClose={togglePanel}
userInput={userInput}
onUserInputChange={setUserInput}
onInputSubmit={handleInputSubmit}
/>
)}
</>
)}

View File

@@ -28,9 +28,10 @@ const Panel = ({
// Render output from EditorArea when available
<>
{terminalOutput.map((line, index) => (
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : line.type === 'input' ? 'terminal-input-line' : 'terminal-output'}`}>
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : line.type === 'input' ? 'terminal-input-line' : line.type === 'prompt' ? 'terminal-prompt-line' : 'terminal-output'}`}>
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''}
{line.type === 'input' ? <span className="terminal-input-marker">[Input]</span> : ''}
{line.type === 'prompt' ? <span className="terminal-prompt-marker">&gt;</span> : ''}
{line.content}
</div>
))}
@@ -40,6 +41,7 @@ const Panel = ({
<span className="terminal-input-marker">Input Required:</span>
</div>
<div className="terminal-input-wrapper">
<div className="terminal-input-prompt">&gt;</div>
<input
type="text"
className="terminal-input"

View File

@@ -0,0 +1,239 @@
import React, { useState, useEffect, useRef } from 'react';
const WebSocketTerminal = ({ code, language, onClose }) => {
const [connected, setConnected] = useState(false);
const [output, setOutput] = useState([]);
const [input, setInput] = useState('');
const [submissionId, setSubmissionId] = useState(null);
const wsRef = useRef(null);
const outputRef = useRef(null);
// Auto-scroll to bottom of output
useEffect(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, [output]);
// Connect to WebSocket on component mount
useEffect(() => {
// Use API URL from environment variable
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
const wsUrl = apiUrl.replace('http://', 'ws://').replace('https://', 'wss://');
// Create WebSocket connection
wsRef.current = new WebSocket(`${wsUrl}/ws`);
// Connection opened
wsRef.current.addEventListener('open', () => {
setConnected(true);
setOutput(prev => [...prev, { type: 'system', content: 'Connected to server' }]);
// Send the code submission
const submission = {
language,
code
};
wsRef.current.send(JSON.stringify(submission));
});
// Listen for messages
wsRef.current.addEventListener('message', (event) => {
const message = event.data;
// Check if this is a submission ID message
if (message.startsWith('Submission ID: ')) {
const id = message.substring('Submission ID: '.length);
setSubmissionId(id);
setOutput(prev => [...prev, { type: 'system', content: `Execution started with ID: ${id}` }]);
return;
}
// Regular output
setOutput(prev => [...prev, { type: 'output', content: message }]);
});
// Connection closed
wsRef.current.addEventListener('close', () => {
setConnected(false);
setOutput(prev => [...prev, { type: 'system', content: 'Disconnected from server' }]);
});
// Connection error
wsRef.current.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
setOutput(prev => [...prev, { type: 'error', content: 'Connection error' }]);
});
// Clean up on unmount
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, [code, language]);
// Handle input submission
const handleInputSubmit = (e) => {
e.preventDefault();
if (!input.trim() || !connected) return;
// Send input to server
wsRef.current.send(input);
// Add input to output display
setOutput(prev => [...prev, { type: 'input', content: input }]);
// Clear input field
setInput('');
};
return (
<div className="websocket-terminal">
<div className="terminal-header">
<div className="terminal-title">
{connected ? 'Connected' : 'Disconnected'}
{submissionId && ` - Execution ID: ${submissionId}`}
</div>
<button className="terminal-close" onClick={onClose}>×</button>
</div>
<div className="terminal-output" ref={outputRef}>
{output.map((line, index) => (
<div key={index} className={`terminal-line ${line.type}`}>
{line.type === 'input' && <span className="input-prefix">&gt; </span>}
{line.content}
</div>
))}
</div>
<form className="terminal-input-form" onSubmit={handleInputSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter input..."
disabled={!connected}
className="terminal-input-field"
/>
<button
type="submit"
disabled={!connected}
className="terminal-input-submit"
>
Send
</button>
</form>
<style jsx>{`
.websocket-terminal {
display: flex;
flex-direction: column;
height: 100%;
background-color: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', monospace;
border-radius: 4px;
overflow: hidden;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #252526;
border-bottom: 1px solid #333;
}
.terminal-title {
font-size: 14px;
}
.terminal-close {
background: none;
border: none;
color: #d4d4d4;
font-size: 18px;
cursor: pointer;
}
.terminal-output {
flex: 1;
padding: 12px;
overflow-y: auto;
font-size: 14px;
line-height: 1.5;
}
.terminal-line {
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.terminal-line.system {
color: #569cd6;
}
.terminal-line.error {
color: #f44747;
}
.terminal-line.input {
color: #ce9178;
}
.input-prefix {
color: #569cd6;
font-weight: bold;
}
.terminal-input-form {
display: flex;
padding: 8px;
background-color: #252526;
border-top: 1px solid #333;
}
.terminal-input-field {
flex: 1;
background-color: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 8px 12px;
font-family: 'Consolas', monospace;
font-size: 14px;
}
.terminal-input-field:focus {
outline: none;
border-color: #007acc;
}
.terminal-input-submit {
margin-left: 8px;
background-color: #0e639c;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
}
.terminal-input-submit:hover {
background-color: #1177bb;
}
.terminal-input-submit:disabled {
background-color: #3c3c3c;
cursor: not-allowed;
}
`}</style>
</div>
);
};
export default WebSocketTerminal;

View File

@@ -441,6 +441,17 @@ body {
font-weight: bold;
}
.terminal-prompt-marker {
color: #569cd6;
margin-right: 8px;
font-weight: bold;
}
.terminal-prompt-line {
color: #569cd6;
font-weight: bold;
}
.terminal-input-line {
color: #4ec9b0;
background-color: rgba(78, 201, 176, 0.1);
@@ -875,7 +886,7 @@ body {
margin-right: 4px;
}
.terminal-toggle-button {
.terminal-toggle-button, .websocket-toggle-button {
background-color: transparent;
border: none;
color: #cccccc;
@@ -886,7 +897,12 @@ body {
align-items: center;
}
.terminal-toggle-button:hover {
.terminal-toggle-button:hover, .websocket-toggle-button:hover {
opacity: 1;
}
.websocket-toggle-button.active {
color: #4ec9b0;
opacity: 1;
}
@@ -969,6 +985,15 @@ body {
.terminal-input-wrapper {
margin-bottom: 8px;
display: flex;
align-items: center;
}
.terminal-input-prompt {
color: #4ec9b0;
font-weight: bold;
margin-right: 8px;
font-size: 18px;
}
.terminal-input-help {
@@ -977,6 +1002,113 @@ body {
font-style: italic;
}
/* WebSocket Terminal */
.websocket-terminal {
display: flex;
flex-direction: column;
height: 100%;
background-color: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', monospace;
border-radius: 4px;
overflow: hidden;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #252526;
border-bottom: 1px solid #333;
}
.terminal-title {
font-size: 14px;
}
.terminal-close {
background: none;
border: none;
color: #d4d4d4;
font-size: 18px;
cursor: pointer;
}
.terminal-output {
flex: 1;
padding: 12px;
overflow-y: auto;
font-size: 14px;
line-height: 1.5;
}
.terminal-line {
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.terminal-line.system {
color: #569cd6;
}
.terminal-line.error {
color: #f44747;
}
.terminal-line.input {
color: #ce9178;
}
.input-prefix {
color: #569cd6;
font-weight: bold;
}
.terminal-input-form {
display: flex;
padding: 8px;
background-color: #252526;
border-top: 1px solid #333;
}
.terminal-input-field {
flex: 1;
background-color: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 8px 12px;
font-family: 'Consolas', monospace;
font-size: 14px;
}
.terminal-input-field:focus {
outline: none;
border-color: #007acc;
}
.terminal-input-submit {
margin-left: 8px;
background-color: #0e639c;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
}
.terminal-input-submit:hover {
background-color: #1177bb;
}
.terminal-input-submit:disabled {
background-color: #3c3c3c;
cursor: not-allowed;
}
.terminal-line.info {
color: #75beff;
}