Files
monaco/Frontend/src/components/CodeChallenge.jsx

751 lines
24 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import Editor from "@monaco-editor/react";
import { Play, Send } from 'lucide-react';
const CodeChallenge = () => {
const [activeQuestion, setActiveQuestion] = useState("Q.1");
const [language, setLanguage] = useState("JavaScript");
const [code, setCode] = useState("");
const [isRunning, setIsRunning] = useState(false);
const [terminalOutput, setTerminalOutput] = useState([]);
const [autoSelected, setAutoSelected] = useState(true);
const [activeSocket, setActiveSocket] = useState(null);
const [submissionId, setSubmissionId] = useState(null);
const socketRef = useRef(null);
// Map frontend language names to backend language identifiers
const getLanguageIdentifier = (uiLanguage) => {
const languageMap = {
'javascript': 'javascript',
'python': 'python',
'java': 'java',
'c++': 'cpp',
'c': 'c'
};
// Important: make sure we convert to lowercase to match the backend's expected format
return languageMap[uiLanguage.toLowerCase()] || uiLanguage.toLowerCase();
};
// Reset execution state to allow rerunning
const resetExecutionState = () => {
setIsRunning(false);
// Properly close the socket if it exists and is open
if (socketRef.current) {
if (socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.close();
}
socketRef.current = null;
}
// Ensure activeSocket is also nullified
setActiveSocket(null);
console.log('Execution state reset, buttons should be enabled');
};
// Example problem data
const problems = {
"Q.1": {
id: "two-sum",
title: "Two Sum",
description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
constraints: "You may assume that each input would have exactly one solution, and you may not use the same element twice.",
examples: [
{
input: "nums = [2,7,11,15], target = 9",
output: "[0,1]",
explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]."
},
{
input: "nums = [3,2,4], target = 6",
output: "[1,2]"
},
{
input: "nums = [3,3], target = 6",
output: "[0,1]"
}
],
starterCode: `/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
// Write your solution here
};`
},
"Q.2": {
id: "palindrome-number",
title: "Palindrome Number",
description: "Given an integer x, return true if x is a palindrome, and false otherwise.",
examples: [
{
input: "x = 121",
output: "true"
},
{
input: "x = -121",
output: "false",
explanation: "From left to right, it reads -121. From right to left, it reads 121-. Therefore it is not a palindrome."
}
],
starterCode: `/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function(x) {
// Write your solution here
};`
},
"Q.3": {
id: "valid-parentheses",
title: "Valid Parentheses",
description: "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.",
constraints: "An input string is valid if: Open brackets must be closed by the same type of brackets. Open brackets must be closed in the correct order.",
examples: [
{
input: 's = "()"',
output: "true"
},
{
input: 's = "()[]{}"',
output: "true"
},
{
input: 's = "(]"',
output: "false"
}
],
starterCode: `/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
// Write your solution here
};`
}
};
// Get appropriate starter code based on language
const getStarterCode = (problem, lang) => {
// Language-specific starter code templates
const templates = {
'JavaScript': problem.starterCode,
'C': `#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// ${problem.title} solution
int main() {
// Write your solution here
return 0;
}`,
'Python': `# ${problem.title}
def solution():
# Write your solution here
# Use input() for user input in Python
# Example: name = input("Enter your name: ")
pass
if __name__ == "__main__":
solution()`,
'Java': `public class Solution {
// ${problem.title}
public static void main(String[] args) {
// Write your solution here
}
}`,
'C++': `#include <iostream>
#include <vector>
using namespace std;
// ${problem.title} solution
int main() {
// Write your solution here
return 0;
}`
};
return templates[lang] || problem.starterCode;
};
// Set initial code based on active problem
useEffect(() => {
if (problems[activeQuestion]) {
setCode(getStarterCode(problems[activeQuestion], language));
}
}, [activeQuestion, language]);
// Cleanup WebSocket connection on unmount
useEffect(() => {
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
// Set a safety timeout to ensure buttons are re-enabled if execution hangs
useEffect(() => {
let safetyTimer = null;
if (isRunning) {
// If execution is running for more than 30 seconds, reset state
safetyTimer = setTimeout(() => {
console.log('Safety timeout reached, re-enabling buttons');
resetExecutionState();
}, 30000);
}
return () => {
if (safetyTimer) clearTimeout(safetyTimer);
};
}, [isRunning]);
// Connect to WebSocket
const connectToWebSocket = (id) => {
console.log('Connecting to WebSocket with ID:', id);
// Force close any existing connections
if (socketRef.current) {
console.log('Closing existing socket, state:', socketRef.current.readyState);
socketRef.current.onclose = null; // Remove existing handler to avoid conflicts
socketRef.current.onerror = null;
socketRef.current.onmessage = null;
if (socketRef.current.readyState !== WebSocket.CLOSED) {
socketRef.current.close();
}
socketRef.current = null;
}
if (activeSocket) {
console.log('Clearing active socket reference');
setActiveSocket(null);
}
console.log('Creating new WebSocket connection');
const wsUrl = `ws://localhost:8080/api/ws/terminal/${id}`;
const socket = new WebSocket(wsUrl);
socket.onopen = () => {
console.log('WebSocket connection established');
setActiveSocket(socket);
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
console.log('WebSocket message received:', message, 'Current isRunning state:', isRunning);
switch (message.type) {
case 'output':
// Handle output message based on the format seen in the screenshot
setTerminalOutput(prev => [
...prev,
{
type: message.content.isError ? 'error' : 'output',
content: message.content.text
}
]);
break;
case 'input_prompt':
// Handle input prompt message (e.g., "Enter your name:")
setTerminalOutput(prev => [
...prev,
{ type: 'output', content: message.content }
]);
break;
case 'status':
let statusText = '';
let statusValue = '';
if (typeof message.content === 'object') {
statusText = `Status: ${message.content.status}`;
statusValue = message.content.status;
} else {
statusText = `Status: ${message.content}`;
statusValue = message.content;
}
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: statusText }
]);
// If status contains "completed" or "failed", stop running
if (statusValue.includes('completed') || statusValue.includes('failed')) {
console.log('Execution completed or failed, stopping');
setTimeout(() => {
setIsRunning(false);
}, 500); // Small delay to ensure UI updates properly
}
break;
case 'error':
let errorContent = '';
if (typeof message.content === 'object' && message.content.message) {
errorContent = message.content.message;
} else {
errorContent = String(message.content);
}
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: errorContent }
]);
console.log('Error received, enabling buttons');
setTimeout(() => {
setIsRunning(false);
}, 500); // Small delay to ensure UI updates properly
break;
case 'system':
const systemContent = String(message.content);
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: systemContent }
]);
// Check for connection closing message which indicates execution is complete
if (systemContent.includes('Connection will close') ||
systemContent.includes('completed successfully') ||
systemContent.includes('Execution completed')) {
console.log('System message indicates completion, enabling buttons');
setTimeout(() => {
setIsRunning(false);
}, 500);
}
break;
default:
// Handle any other message types or direct string content
console.log('Unknown message type:', message);
if (typeof message === 'object') {
setTerminalOutput(prev => [
...prev,
{ type: 'output', content: JSON.stringify(message) }
]);
}
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: 'WebSocket connection error' }
]);
console.log('WebSocket error, enabling buttons');
setTimeout(() => {
setIsRunning(false);
}, 500); // Small delay to ensure UI updates properly
};
socket.onclose = () => {
console.log('WebSocket connection closed');
setActiveSocket(null);
// Ensure buttons are re-enabled when the connection closes
setTimeout(() => {
resetExecutionState();
}, 100);
};
// Set the socket reference early to ensure we can clean it up if needed
socketRef.current = socket;
return socket;
};
// Handle code execution
const runCode = async () => {
console.log('Run button clicked, current state:', {
isRunning,
socketState: activeSocket ? activeSocket.readyState : 'no socket',
socketRefState: socketRef.current ? socketRef.current.readyState : 'no socket ref'
});
// First make sure previous connections are fully closed
resetExecutionState();
// Increase the delay to ensure clean state before starting new execution
setTimeout(async () => {
// Double-check socket state before proceeding
if (activeSocket || socketRef.current) {
console.warn('Socket still exists after reset, forcing cleanup');
if (activeSocket && activeSocket.readyState !== WebSocket.CLOSED) {
activeSocket.close();
}
if (socketRef.current && socketRef.current.readyState !== WebSocket.CLOSED) {
socketRef.current.close();
}
socketRef.current = null;
setActiveSocket(null);
// Extra delay to ensure socket is fully closed
await new Promise(resolve => setTimeout(resolve, 100));
}
setIsRunning(true);
setTerminalOutput([
{ type: 'system', content: `Running ${problems[activeQuestion].id}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: getLanguageIdentifier(language),
input: '',
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
} catch (error) {
console.error('Error submitting code:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Error: ${error.message}` }
]);
resetExecutionState();
}
}, 200); // Increased delay to ensure clean state
};
// Handle code submission
const submitCode = async () => {
setIsRunning(true);
setTerminalOutput([
{ type: 'system', content: `Submitting solution for ${problems[activeQuestion].id}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: getLanguageIdentifier(language),
input: '',
problemId: problems[activeQuestion].id
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
} catch (error) {
console.error('Error submitting solution:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Error: ${error.message}` }
]);
setIsRunning(false);
}
};
// Render the current problem
const renderProblem = () => {
const problem = problems[activeQuestion];
if (!problem) return null;
return (
<div className="problem-container">
<h1>{problem.title}</h1>
<div className="problem-description">
<p>{problem.description}</p>
{problem.constraints && <p>{problem.constraints}</p>}
</div>
{/* Test cases section removed */}
</div>
);
};
// Add this useEffect to monitor socket state
useEffect(() => {
// If we have an active socket but aren't running, we should clean up
if (activeSocket && !isRunning) {
console.log('Cleaning up inactive socket');
if (activeSocket.readyState === WebSocket.OPEN) {
activeSocket.close();
}
setActiveSocket(null);
}
}, [activeSocket, isRunning]);
return (
<div className="code-challenge-container">
<header className="code-challenge-header">
<h1>OnScreen Test</h1>
<button className="sign-in-btn">Sign In</button>
</header>
{/* <div className="code-challenge-problem-nav">
<h3 className="problem-number">1. {problems["Q.1"].title}</h3>
</div> */}
<div className="code-challenge-main">
<div className="problem-tabs">
<button
className={activeQuestion === "Q.1" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.1")}
>
Q.1
</button>
<button
className={activeQuestion === "Q.2" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.2")}
>
Q.2
</button>
<button
className={activeQuestion === "Q.3" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.3")}
>
Q.3
</button>
</div>
<div className="problem-content">
{renderProblem()}
</div>
<div className="editor-section">
<div className="editor-header">
<div className="editor-controls">
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
className="language-selector"
>
<option value="JavaScript">JavaScript</option>
<option value="Python">Python</option>
<option value="Java">Java</option>
<option value="C++">C++</option>
<option value="C">C</option>
</select>
</div>
<div className="editor-actions">
<button
className="run-btn"
onClick={runCode}
disabled={isRunning}
title={isRunning ? "Code execution in progress..." : "Run code"}
>
{isRunning ? (
<>
<span className="loading-spinner"></span> Running...
</>
) : (
<>
<Play size={16} /> Run
</>
)}
</button>
<button
className="submit-btn"
onClick={submitCode}
disabled={isRunning}
title={isRunning ? "Code execution in progress..." : "Submit solution"}
>
{isRunning ? (
<>
<span className="loading-spinner"></span> Submitting...
</>
) : (
<>
<Send size={16} /> Submit
</>
)}
</button>
</div>
</div>
<div className="editor-container">
<Editor
height="100%"
defaultLanguage="python"
language={language.toLowerCase() === 'c++' ? 'cpp' : language.toLowerCase()}
value={code}
onChange={(value) => setCode(value)}
theme="hc-black"
options={{
fontSize: 14,
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</div>
</div>
</div>
<div className="terminal-section">
<div className="terminal-header">
<span>Terminal</span>
{/* <div className="terminal-controls">
<button className="terminal-btn">⊞</button>
<button className="terminal-btn">□</button>
<button className="terminal-btn">✕</button>
</div> */}
</div>
<div className="terminal-content">
{terminalOutput.map((line, index) => (
<div
key={index}
className={`terminal-line ${line.type}`}
>
{line.content}
</div>
))}
<div className="terminal-prompt">
<span className="prompt-symbol">$</span>
<input
type="text"
className="terminal-input"
placeholder="Type here..."
disabled={!isRunning}
// Update the ref callback
ref={(inputEl) => {
// Auto-focus input when isRunning changes to true
if (inputEl && isRunning) {
inputEl.focus();
// Clear any previous input
inputEl.value = '';
}
}}
onKeyDown={(e) => { // Change from onKeyPress to onKeyDown for better cross-browser support
if (e.key === 'Enter') {
e.preventDefault(); // Prevent default to avoid form submissions
const input = e.target.value.trim();
if (!input) return; // Skip empty input
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
try {
// Send input to server
activeSocket.send(JSON.stringify({
"type": "input",
"content": input
}));
// Add input to terminal output
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: `$ ${input}` }
]);
// Clear the input field
e.target.value = '';
} catch (error) {
console.error("Error sending input:", error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Failed to send input: ${error.message}` }
]);
}
} else {
// Better error message with socket state information
const socketState = activeSocket ?
['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][activeSocket.readyState] :
'NO_SOCKET';
console.log(`Cannot send input: Socket state is ${socketState}`);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Cannot send input: connection not available (${socketState})` }
]);
}
}
}}
onKeyPress={(e) => {
if (e.key === 'Enter' && activeSocket && activeSocket.readyState === WebSocket.OPEN) {
const input = e.target.value;
// Send input to WebSocket with the correct format
try {
activeSocket.send(JSON.stringify({
"type": "input",
"content": input
}));
// Add input to terminal output
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: `$ ${input}` }
]);
// Clear the input field
e.target.value = '';
} catch (error) {
console.error("Error sending input:", error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Failed to send input: ${error.message}` }
]);
}
} else if (e.key === 'Enter') {
// Inform user if socket isn't available
if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) {
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Cannot send input: connection closed` }
]);
}
}
}}
/>
</div>
</div>
</div>
</div>
);
};
export default CodeChallenge;