1144 lines
41 KiB
JavaScript
1144 lines
41 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import Editor from "@monaco-editor/react";
|
|
import { Play, Send } from 'lucide-react';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
const CodeChallenge = () => {
|
|
const [test, setTest] = useState(null);
|
|
const [questions, setQuestions] = useState([]);
|
|
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 [timeRemaining, setTimeRemaining] = useState(null);
|
|
const [terminalInput, setTerminalInput] = useState('');
|
|
const [waitingForInput, setWaitingForInput] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('console'); // 'console' or 'testcases'
|
|
const [markedForReview, setMarkedForReview] = useState({}); // Track questions marked for review
|
|
const socketRef = useRef(null);
|
|
const terminalInputRef = useRef(null);
|
|
const { token } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
// Load test data from localStorage
|
|
useEffect(() => {
|
|
const testData = localStorage.getItem('currentTest');
|
|
if (testData) {
|
|
try {
|
|
const parsedData = JSON.parse(testData);
|
|
console.log('Loaded test data:', parsedData);
|
|
console.log('End time in test data:', parsedData.end_time);
|
|
setTest(parsedData);
|
|
if (parsedData.questions && parsedData.questions.length > 0) {
|
|
setQuestions(parsedData.questions);
|
|
// Set initial code from first question
|
|
const firstQuestion = parsedData.questions[0];
|
|
setLanguage(firstQuestion.programming_language || 'JavaScript');
|
|
setCode(firstQuestion.code_template || getDefaultTemplate(firstQuestion.programming_language || 'JavaScript'));
|
|
}
|
|
|
|
// Load marked for review questions from localStorage
|
|
const markedKey = `marked_for_review_${parsedData.id}`;
|
|
const savedMarked = localStorage.getItem(markedKey);
|
|
if (savedMarked) {
|
|
try {
|
|
setMarkedForReview(JSON.parse(savedMarked));
|
|
} catch (err) {
|
|
console.error('Error loading marked questions:', err);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading test data:', error);
|
|
}
|
|
} else {
|
|
// No test data, redirect back to tests
|
|
navigate('/tests');
|
|
}
|
|
}, [navigate]);
|
|
|
|
// Timer countdown
|
|
useEffect(() => {
|
|
if (!test) return;
|
|
|
|
// Check for different possible time field names
|
|
const endTimeValue = test.end_time || test.endTime || test.end_date;
|
|
|
|
if (!endTimeValue) {
|
|
console.warn('No end time found in test data:', test);
|
|
return;
|
|
}
|
|
|
|
const updateTimer = () => {
|
|
const now = new Date();
|
|
let endTime;
|
|
|
|
// Try to parse the end time - handle different formats
|
|
try {
|
|
endTime = new Date(endTimeValue);
|
|
|
|
// Check if the date is valid
|
|
if (isNaN(endTime.getTime())) {
|
|
console.error('Invalid date format:', endTimeValue);
|
|
setTimeRemaining('Invalid Date');
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error parsing end time:', error);
|
|
setTimeRemaining('Invalid Date');
|
|
return;
|
|
}
|
|
|
|
const diff = endTime - now;
|
|
|
|
if (diff <= 0) {
|
|
setTimeRemaining('Time Up!');
|
|
// Optionally auto-submit or redirect
|
|
return;
|
|
}
|
|
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
|
|
setTimeRemaining(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
|
|
};
|
|
|
|
updateTimer(); // Initial call
|
|
const timerId = setInterval(updateTimer, 1000);
|
|
|
|
return () => clearInterval(timerId);
|
|
}, [test]);
|
|
|
|
// Get default code template for a language
|
|
const getDefaultTemplate = (lang) => {
|
|
const templates = {
|
|
'JavaScript': '// Write your code here\n',
|
|
'Python': '# Write your code here\n',
|
|
'Java': 'public class Solution {\n public static void main(String[] args) {\n // Write your code here\n }\n}',
|
|
'C++': '#include <iostream>\nusing namespace std;\n\nint main() {\n // Write your code here\n return 0;\n}',
|
|
'C': '#include <stdio.h>\n\nint main() {\n // Write your code here\n return 0;\n}'
|
|
};
|
|
return templates[lang] || '// Write your code here\n';
|
|
};
|
|
|
|
// Map question index to Q.1, Q.2, Q.3 format
|
|
const getQuestionIndex = (questionKey) => {
|
|
const index = parseInt(questionKey.replace('Q.', '')) - 1;
|
|
return index;
|
|
};
|
|
|
|
// Get current question based on activeQuestion
|
|
const getCurrentQuestion = () => {
|
|
const index = getQuestionIndex(activeQuestion);
|
|
return questions[index] || 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);
|
|
setWaitingForInput(false);
|
|
setTerminalInput('');
|
|
|
|
// 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');
|
|
};
|
|
|
|
// Handle terminal input submission
|
|
const handleTerminalInput = (e) => {
|
|
e.preventDefault();
|
|
|
|
if (!terminalInput.trim()) {
|
|
return;
|
|
}
|
|
|
|
// Display the input in terminal
|
|
setTerminalOutput(prev => [
|
|
...prev,
|
|
{ type: 'input', content: terminalInput }
|
|
]);
|
|
|
|
// If socket is available, send input to backend
|
|
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
|
|
activeSocket.send(JSON.stringify({
|
|
type: 'input',
|
|
content: terminalInput + '\n'
|
|
}));
|
|
}
|
|
|
|
// Clear input
|
|
setTerminalInput('');
|
|
};
|
|
|
|
// Set initial code based on active problem
|
|
useEffect(() => {
|
|
const currentQuestion = getCurrentQuestion();
|
|
if (currentQuestion) {
|
|
// Check if there's a saved submission for this question
|
|
const savedSubmission = localStorage.getItem(`submission_${test?.id}_${currentQuestion.id}`);
|
|
if (savedSubmission) {
|
|
try {
|
|
const submission = JSON.parse(savedSubmission);
|
|
setCode(submission.code);
|
|
setLanguage(currentQuestion.programming_language || 'JavaScript');
|
|
setTerminalOutput([
|
|
{ type: 'system', content: `Loaded your previous submission from ${new Date(submission.timestamp).toLocaleString()}` }
|
|
]);
|
|
} catch (error) {
|
|
console.error('Error loading saved submission:', error);
|
|
setLanguage(currentQuestion.programming_language || 'JavaScript');
|
|
setCode(currentQuestion.code_template || getDefaultTemplate(currentQuestion.programming_language || 'JavaScript'));
|
|
}
|
|
} else {
|
|
setLanguage(currentQuestion.programming_language || 'JavaScript');
|
|
setCode(currentQuestion.code_template || getDefaultTemplate(currentQuestion.programming_language || 'JavaScript'));
|
|
setTerminalOutput([]);
|
|
}
|
|
}
|
|
}, [activeQuestion]);
|
|
|
|
// 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 apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, '');
|
|
const wsUrl = `${wsProtocol}//${wsBaseUrl}/api/ws/terminal/${id}`;
|
|
const socket = new WebSocket(wsUrl);
|
|
|
|
socket.onopen = () => {
|
|
console.log('WebSocket connection established');
|
|
setActiveSocket(socket);
|
|
setWaitingForInput(true);
|
|
// Focus the input field when connection is ready
|
|
setTimeout(() => {
|
|
if (terminalInputRef.current) {
|
|
terminalInputRef.current.focus();
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
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:")
|
|
console.log('Input prompt received');
|
|
setTerminalOutput(prev => [
|
|
...prev,
|
|
{ type: 'output', content: message.content }
|
|
]);
|
|
// Ensure input is enabled and focused
|
|
setWaitingForInput(true);
|
|
setTimeout(() => {
|
|
if (terminalInputRef.current) {
|
|
terminalInputRef.current.focus();
|
|
}
|
|
}, 50);
|
|
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);
|
|
setWaitingForInput(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');
|
|
setWaitingForInput(false);
|
|
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);
|
|
const currentQuestion = getCurrentQuestion();
|
|
setTerminalOutput([
|
|
{ type: 'system', content: `Running ${currentQuestion?.question_text || currentQuestion?.title || 'code'}...` }
|
|
]);
|
|
|
|
try {
|
|
// Submit code to the backend
|
|
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
|
|
|
// Ensure code has actual newlines, not escaped \n strings
|
|
// This handles cases where code might have literal "\n" instead of newlines
|
|
const processedCode = code.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
|
|
const response = await fetch(`${apiUrl}/api/submit`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
code: processedCode,
|
|
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);
|
|
const currentQuestion = getCurrentQuestion();
|
|
setTerminalOutput([
|
|
{ type: 'system', content: `Submitting solution for ${currentQuestion?.question_text || currentQuestion?.title || 'problem'}...` }
|
|
]);
|
|
|
|
try {
|
|
// If we have real test data, submit to faculty backend
|
|
if (currentQuestion && test) {
|
|
const apiUrl = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
|
const response = await fetch(`${apiUrl}/students/submissions`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
testId: test.id,
|
|
answers: [{
|
|
questionId: currentQuestion.id,
|
|
submittedAnswer: code
|
|
}]
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
setTerminalOutput(prev => [
|
|
...prev,
|
|
{ type: 'system', content: '✓ Submission successful!' },
|
|
{ type: 'output', content: `Your answer has been submitted for Question ${getQuestionIndex(activeQuestion) + 1}` },
|
|
{ type: 'output', content: `Test: ${test.title}` },
|
|
{ type: 'system', content: 'You can modify and resubmit your answer anytime before the test ends.' }
|
|
]);
|
|
|
|
// Store submission locally
|
|
localStorage.setItem(`submission_${test.id}_${currentQuestion.id}`, JSON.stringify({
|
|
code,
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
} else {
|
|
throw new Error(data.message || 'Submission failed');
|
|
}
|
|
|
|
setIsRunning(false);
|
|
return;
|
|
}
|
|
|
|
// If no test data, show error
|
|
throw new Error('No test data available. Please start a test from the test list.');
|
|
|
|
} catch (error) {
|
|
console.error('Error submitting solution:', error);
|
|
setTerminalOutput(prev => [
|
|
...prev,
|
|
{ type: 'error', content: `Error: ${error.message}` }
|
|
]);
|
|
setIsRunning(false);
|
|
}
|
|
};
|
|
|
|
// Handle save and move to next question
|
|
const handleSaveAndNext = async () => {
|
|
const currentQuestion = getCurrentQuestion();
|
|
|
|
if (!currentQuestion || !test) {
|
|
alert('No test data available.');
|
|
return;
|
|
}
|
|
|
|
// Save current answer
|
|
try {
|
|
const apiUrl = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
|
const response = await fetch(`${apiUrl}/students/submissions`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
testId: test.id,
|
|
answers: [{
|
|
questionId: currentQuestion.id,
|
|
submittedAnswer: code
|
|
}]
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Store submission locally
|
|
localStorage.setItem(`submission_${test.id}_${currentQuestion.id}`, JSON.stringify({
|
|
code,
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
|
|
setTerminalOutput([
|
|
{ type: 'system', content: `✓ Answer saved for Question ${getQuestionIndex(activeQuestion) + 1}` }
|
|
]);
|
|
|
|
// Move to next question if available
|
|
const currentIndex = getQuestionIndex(activeQuestion);
|
|
if (currentIndex < questions.length - 1) {
|
|
const nextQuestionKey = `Q.${currentIndex + 2}`;
|
|
setActiveQuestion(nextQuestionKey);
|
|
} else {
|
|
// If last question, show message
|
|
setTerminalOutput([
|
|
{ type: 'system', content: '✓ Answer saved!' },
|
|
{ type: 'output', content: 'This is the last question. Click "Submit Test" to finish.' }
|
|
]);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error saving answer:', error);
|
|
alert(`Error saving answer: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Handle reset code
|
|
const handleResetCode = () => {
|
|
const currentQuestion = getCurrentQuestion();
|
|
if (!currentQuestion) return;
|
|
|
|
const confirmReset = window.confirm(
|
|
'Are you sure you want to reset the code? This will restore the original starter code and you will lose all your current changes.'
|
|
);
|
|
|
|
if (!confirmReset) return;
|
|
|
|
// Clear saved submission from localStorage
|
|
if (test && currentQuestion) {
|
|
localStorage.removeItem(`submission_${test.id}_${currentQuestion.id}`);
|
|
}
|
|
|
|
// Reset to original code template
|
|
const originalCode = currentQuestion.code_template || getDefaultTemplate(currentQuestion.programming_language || language);
|
|
setCode(originalCode);
|
|
|
|
// Clear terminal output
|
|
setTerminalOutput([
|
|
{ type: 'system', content: '✓ Code reset to original starter code' }
|
|
]);
|
|
};
|
|
|
|
// Handle mark for review
|
|
const handleMarkForReview = () => {
|
|
if (!test) return;
|
|
|
|
const currentIndex = getQuestionIndex(activeQuestion);
|
|
const currentQuestion = getCurrentQuestion();
|
|
if (!currentQuestion) return;
|
|
|
|
const questionId = currentQuestion.id;
|
|
const isCurrentlyMarked = markedForReview[questionId];
|
|
|
|
// Toggle marked status
|
|
const updatedMarked = {
|
|
...markedForReview,
|
|
[questionId]: !isCurrentlyMarked
|
|
};
|
|
|
|
setMarkedForReview(updatedMarked);
|
|
|
|
// Save to localStorage
|
|
const markedKey = `marked_for_review_${test.id}`;
|
|
localStorage.setItem(markedKey, JSON.stringify(updatedMarked));
|
|
|
|
// Show feedback
|
|
setTerminalOutput([
|
|
{
|
|
type: 'system',
|
|
content: !isCurrentlyMarked
|
|
? `✓ Question ${currentIndex + 1} marked for review`
|
|
: `✓ Question ${currentIndex + 1} unmarked for review`
|
|
}
|
|
]);
|
|
};
|
|
|
|
// Handle final test submission
|
|
const handleSubmitTest = async () => {
|
|
if (!test) {
|
|
alert('No test data available.');
|
|
return;
|
|
}
|
|
|
|
const confirmSubmit = window.confirm(
|
|
'Are you sure you want to submit the entire test? You will not be able to make any changes after submission.'
|
|
);
|
|
|
|
if (!confirmSubmit) return;
|
|
|
|
try {
|
|
// Save the current question's answer before submitting
|
|
const currentQuestion = getCurrentQuestion();
|
|
if (currentQuestion && code.trim()) {
|
|
const apiUrl = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
|
try {
|
|
const saveResponse = await fetch(`${apiUrl}/students/submissions`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
testId: test.id,
|
|
answers: [{
|
|
questionId: currentQuestion.id,
|
|
submittedAnswer: code
|
|
}]
|
|
}),
|
|
});
|
|
|
|
if (saveResponse.ok) {
|
|
console.log('Current answer saved successfully');
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to save current answer:', err);
|
|
}
|
|
}
|
|
|
|
// For now, just mark test as completed locally and redirect
|
|
// TODO: Implement backend endpoint for test submission
|
|
alert('Test submitted successfully! You will be redirected to the test list.');
|
|
|
|
// Mark test as submitted locally
|
|
localStorage.setItem(`test_${test.id}_submitted`, JSON.stringify({
|
|
submittedAt: new Date().toISOString(),
|
|
testId: test.id
|
|
}));
|
|
|
|
// Clear test data
|
|
localStorage.removeItem('currentTest');
|
|
|
|
// Redirect to tests page
|
|
navigate('/tests');
|
|
|
|
} catch (error) {
|
|
console.error('Error submitting test:', error);
|
|
alert(`Error submitting test: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Render the current problem
|
|
const renderProblem = () => {
|
|
const currentQuestion = getCurrentQuestion();
|
|
|
|
// If we have real test question, use it
|
|
if (currentQuestion) {
|
|
return (
|
|
<div className="problem-container">
|
|
<div className="problem-description">
|
|
<p>{currentQuestion.question_text || currentQuestion.description}</p>
|
|
{currentQuestion.constraints && <p><strong>Constraints:</strong> {currentQuestion.constraints}</p>}
|
|
{currentQuestion.marks && <p><strong>Points:</strong> {currentQuestion.marks}</p>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// No question data available
|
|
return (
|
|
<div className="problem-container">
|
|
<div className="problem-description">
|
|
<p>No question data available. Please ensure a test is selected.</p>
|
|
</div>
|
|
</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">
|
|
<div className="header-left">
|
|
<div className="header-icon">📝</div>
|
|
<h1>{test?.title || 'CS101: Midterm Examination'}</h1>
|
|
</div>
|
|
<div className="header-right">
|
|
<span className="saved-indicator">All changes saved ✓</span>
|
|
<button className="submit-test-btn" onClick={handleSubmitTest}>Submit Test</button>
|
|
{/* <div className="user-profile">
|
|
<div className="user-avatar">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="12" cy="7" r="4"></circle>
|
|
</svg>
|
|
</div>
|
|
</div> */}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="code-challenge-body">
|
|
{/* Left Sidebar */}
|
|
<div className="left-sidebar">
|
|
{/* Time Remaining */}
|
|
<div className="time-remaining-widget">
|
|
<h3>Time Remaining</h3>
|
|
<div className="timer-value">
|
|
{timeRemaining && timeRemaining !== 'Time Up!' ? (
|
|
<>
|
|
<div className="time-unit">
|
|
<span className="time-block">{timeRemaining.split(':')[0]}</span>
|
|
<span className="time-label">Hours</span>
|
|
</div>
|
|
<div className="time-unit">
|
|
<span className="time-block">{timeRemaining.split(':')[1]}</span>
|
|
<span className="time-label">Minutes</span>
|
|
</div>
|
|
<div className="time-unit">
|
|
<span className="time-block">{timeRemaining.split(':')[2]}</span>
|
|
<span className="time-label">Seconds</span>
|
|
</div>
|
|
</>
|
|
) : timeRemaining === 'Time Up!' ? (
|
|
<span className="time-up">{timeRemaining}</span>
|
|
) : (
|
|
<>
|
|
<div className="time-unit">
|
|
<span className="time-block">00</span>
|
|
<span className="time-label">Hours</span>
|
|
</div>
|
|
<div className="time-unit">
|
|
<span className="time-block">00</span>
|
|
<span className="time-label">Minutes</span>
|
|
</div>
|
|
<div className="time-unit">
|
|
<span className="time-block">00</span>
|
|
<span className="time-label">Seconds</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Question Palette */}
|
|
<div className="question-palette-widget">
|
|
<h3>Question Palette</h3>
|
|
<div className="question-palette-grid">
|
|
{(questions.length > 0 ? questions : Array.from({length: 20}, (_, i) => i + 1)).map((q, idx) => {
|
|
const questionKey = `Q.${idx + 1}`;
|
|
const questionNum = idx + 1;
|
|
const question = questions[idx];
|
|
const isMarked = question && markedForReview[question.id];
|
|
|
|
return (
|
|
<button
|
|
key={questionKey}
|
|
className={`question-palette-btn ${activeQuestion === questionKey ? "palette-active" : ""} ${isMarked ? "palette-marked" : ""}`}
|
|
onClick={() => setActiveQuestion(questionKey)}
|
|
disabled={questions.length > 0 && idx >= questions.length}
|
|
>
|
|
{questionNum}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="palette-legend">
|
|
<div className="legend-item">
|
|
<div className="legend-dot legend-current"></div>
|
|
<span>Current</span>
|
|
</div>
|
|
<div className="legend-item">
|
|
<div className="legend-dot legend-answered"></div>
|
|
<span>Answered</span>
|
|
</div>
|
|
<div className="legend-item">
|
|
<div className="legend-dot legend-skipped"></div>
|
|
<span>Skipped</span>
|
|
</div>
|
|
<div className="legend-item">
|
|
<div className="legend-dot legend-marked"></div>
|
|
<span>Marked for Review</span>
|
|
</div>
|
|
<div className="legend-item">
|
|
<div className="legend-dot legend-not-visited"></div>
|
|
<span>Not Visited</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content Area */}
|
|
<div className="main-content-area">
|
|
{/* Top Section: Question text above editor */}
|
|
<div className="top-section">
|
|
<div className="content-wrapper">
|
|
{/* Question Text Bar */}
|
|
<div className="question-bar">
|
|
<div className="question-header">
|
|
<h2>Question {activeQuestion.replace('Q.', '')} of {questions.length || 20}</h2>
|
|
</div>
|
|
<div className="question-text">
|
|
{renderProblem()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Code Editor */}
|
|
<div className="editor-section-wrapper">
|
|
<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>
|
|
<button
|
|
className="reset-code-btn"
|
|
onClick={handleResetCode}
|
|
disabled={isRunning}
|
|
title="Reset code to original starter template"
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<polyline points="16 18 22 12 16 6"></polyline>
|
|
<polyline points="8 6 2 12 8 18"></polyline>
|
|
</svg>
|
|
Reset Code
|
|
</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>
|
|
|
|
{/* Bottom Section: Console/Testcases */}
|
|
<div className="bottom-section">
|
|
<div className="console-section">
|
|
<div className="console-header">
|
|
<div className="console-tabs">
|
|
<button
|
|
className={`console-tab ${activeTab === 'console' ? 'console-tab-active' : ''}`}
|
|
onClick={() => setActiveTab('console')}
|
|
>
|
|
<span>Console</span>
|
|
</button>
|
|
<button
|
|
className={`console-tab ${activeTab === 'testcases' ? 'console-tab-active' : ''}`}
|
|
onClick={() => setActiveTab('testcases')}
|
|
>
|
|
<span>Test Cases</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Console Tab Content */}
|
|
{activeTab === 'console' && (
|
|
<div
|
|
className="console-content"
|
|
onClick={() => {
|
|
if (waitingForInput && terminalInputRef.current) {
|
|
terminalInputRef.current.focus();
|
|
}
|
|
}}
|
|
style={{ cursor: waitingForInput ? 'text' : 'default' }}
|
|
>
|
|
{terminalOutput.length === 0 ? (
|
|
<div className="console-placeholder">
|
|
Console output will appear here...
|
|
</div>
|
|
) : (
|
|
<>
|
|
{terminalOutput.map((line, index) => (
|
|
<div
|
|
key={index}
|
|
className={`console-line ${line.type}`}
|
|
>
|
|
{line.content}
|
|
</div>
|
|
))}
|
|
{waitingForInput && (
|
|
<div className="console-line console-input-line">
|
|
<form onSubmit={handleTerminalInput} className="console-input-form">
|
|
<span className="console-cursor">{'> '}</span>
|
|
<input
|
|
ref={terminalInputRef}
|
|
type="text"
|
|
value={terminalInput}
|
|
onChange={(e) => setTerminalInput(e.target.value)}
|
|
className="console-input"
|
|
autoFocus
|
|
/>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Test Cases Tab Content */}
|
|
{activeTab === 'testcases' && (
|
|
<div className="console-content testcases-tab-content">
|
|
{(() => {
|
|
const currentQuestion = getCurrentQuestion();
|
|
if (currentQuestion?.test_cases && currentQuestion.test_cases.length > 0) {
|
|
return (
|
|
<div className="testcases-container">
|
|
{currentQuestion.test_cases.map((testCase, idx) => (
|
|
<div key={testCase.id || idx} className="testcase-item">
|
|
<div className="testcase-header">
|
|
<span className="testcase-title">Test Case {idx + 1}</span>
|
|
{testCase.is_sample && (
|
|
<span className="testcase-badge">Sample</span>
|
|
)}
|
|
</div>
|
|
<div className="testcase-body">
|
|
<div className="testcase-field">
|
|
<div className="testcase-field-label">Input:</div>
|
|
<pre className="testcase-field-value">{testCase.input}</pre>
|
|
</div>
|
|
<div className="testcase-field">
|
|
<div className="testcase-field-label">Expected Output:</div>
|
|
<pre className="testcase-field-value">{testCase.expected_output}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
} else {
|
|
return (
|
|
<div className="console-placeholder">
|
|
No test cases available for this question.
|
|
</div>
|
|
);
|
|
}
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons at Bottom Right */}
|
|
<div className="action-bar">
|
|
<button
|
|
className="action-bar-btn action-mark"
|
|
onClick={handleMarkForReview}
|
|
disabled={!getCurrentQuestion()}
|
|
>
|
|
{(() => {
|
|
const currentQuestion = getCurrentQuestion();
|
|
const isMarked = currentQuestion && markedForReview[currentQuestion.id];
|
|
return (
|
|
<>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill={isMarked ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
|
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
|
</svg>
|
|
{isMarked ? 'Unmark Review' : 'Mark for Review'}
|
|
</>
|
|
);
|
|
})()}
|
|
</button>
|
|
<button
|
|
className="action-bar-btn action-run"
|
|
onClick={runCode}
|
|
disabled={isRunning}
|
|
>
|
|
{isRunning ? (
|
|
<>
|
|
<span className="loading-spinner"></span>
|
|
Running...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play size={16} />
|
|
Run Code
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
className="action-bar-btn action-save"
|
|
onClick={handleSaveAndNext}
|
|
>
|
|
Save & Next
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M5 12h14"/>
|
|
<path d="m12 5 7 7-7 7"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CodeChallenge;
|