add terminal input handling and styling to CodeChallenge component
This commit is contained in:
@@ -16,7 +16,10 @@ const CodeChallenge = () => {
|
|||||||
const [activeSocket, setActiveSocket] = useState(null);
|
const [activeSocket, setActiveSocket] = useState(null);
|
||||||
const [submissionId, setSubmissionId] = useState(null);
|
const [submissionId, setSubmissionId] = useState(null);
|
||||||
const [timeRemaining, setTimeRemaining] = useState(null);
|
const [timeRemaining, setTimeRemaining] = useState(null);
|
||||||
|
const [terminalInput, setTerminalInput] = useState('');
|
||||||
|
const [waitingForInput, setWaitingForInput] = useState(false);
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
|
const terminalInputRef = useRef(null);
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -26,6 +29,8 @@ const CodeChallenge = () => {
|
|||||||
if (testData) {
|
if (testData) {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(testData);
|
const parsedData = JSON.parse(testData);
|
||||||
|
console.log('Loaded test data:', parsedData);
|
||||||
|
console.log('End time in test data:', parsedData.end_time);
|
||||||
setTest(parsedData);
|
setTest(parsedData);
|
||||||
if (parsedData.questions && parsedData.questions.length > 0) {
|
if (parsedData.questions && parsedData.questions.length > 0) {
|
||||||
setQuestions(parsedData.questions);
|
setQuestions(parsedData.questions);
|
||||||
@@ -45,11 +50,36 @@ const CodeChallenge = () => {
|
|||||||
|
|
||||||
// Timer countdown
|
// Timer countdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!test || !test.end_time) return;
|
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 updateTimer = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const endTime = new Date(test.end_time);
|
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;
|
const diff = endTime - now;
|
||||||
|
|
||||||
if (diff <= 0) {
|
if (diff <= 0) {
|
||||||
@@ -111,6 +141,8 @@ const CodeChallenge = () => {
|
|||||||
// Reset execution state to allow rerunning
|
// Reset execution state to allow rerunning
|
||||||
const resetExecutionState = () => {
|
const resetExecutionState = () => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
|
setWaitingForInput(false);
|
||||||
|
setTerminalInput('');
|
||||||
|
|
||||||
// Properly close the socket if it exists and is open
|
// Properly close the socket if it exists and is open
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
@@ -126,6 +158,32 @@ const CodeChallenge = () => {
|
|||||||
console.log('Execution state reset, buttons should be enabled');
|
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('');
|
||||||
|
};
|
||||||
|
|
||||||
// Example problem data
|
// Example problem data
|
||||||
const problems = {
|
const problems = {
|
||||||
"Q.1": {
|
"Q.1": {
|
||||||
@@ -348,6 +406,13 @@ int main() {
|
|||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
console.log('WebSocket connection established');
|
console.log('WebSocket connection established');
|
||||||
setActiveSocket(socket);
|
setActiveSocket(socket);
|
||||||
|
setWaitingForInput(true);
|
||||||
|
// Focus the input field when connection is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
if (terminalInputRef.current) {
|
||||||
|
terminalInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
@@ -369,10 +434,18 @@ int main() {
|
|||||||
|
|
||||||
case 'input_prompt':
|
case 'input_prompt':
|
||||||
// Handle input prompt message (e.g., "Enter your name:")
|
// Handle input prompt message (e.g., "Enter your name:")
|
||||||
|
console.log('Input prompt received');
|
||||||
setTerminalOutput(prev => [
|
setTerminalOutput(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ type: 'output', content: message.content }
|
{ type: 'output', content: message.content }
|
||||||
]);
|
]);
|
||||||
|
// Ensure input is enabled and focused
|
||||||
|
setWaitingForInput(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (terminalInputRef.current) {
|
||||||
|
terminalInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'status':
|
case 'status':
|
||||||
@@ -434,6 +507,7 @@ int main() {
|
|||||||
console.log('System message indicates completion, enabling buttons');
|
console.log('System message indicates completion, enabling buttons');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
|
setWaitingForInput(false);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -461,6 +535,7 @@ int main() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('WebSocket error, enabling buttons');
|
console.log('WebSocket error, enabling buttons');
|
||||||
|
setWaitingForInput(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
}, 500); // Small delay to ensure UI updates properly
|
}, 500); // Small delay to ensure UI updates properly
|
||||||
@@ -622,6 +697,67 @@ int main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 final test submission
|
// Handle final test submission
|
||||||
const handleSubmitTest = async () => {
|
const handleSubmitTest = async () => {
|
||||||
if (!test) {
|
if (!test) {
|
||||||
@@ -933,20 +1069,45 @@ int main() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="console-content">
|
<div
|
||||||
|
className="console-content"
|
||||||
|
onClick={() => {
|
||||||
|
if (waitingForInput && terminalInputRef.current) {
|
||||||
|
terminalInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ cursor: waitingForInput ? 'text' : 'default' }}
|
||||||
|
>
|
||||||
{terminalOutput.length === 0 ? (
|
{terminalOutput.length === 0 ? (
|
||||||
<div className="console-placeholder">
|
<div className="console-placeholder">
|
||||||
Console output will appear here...
|
Console output will appear here...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
terminalOutput.map((line, index) => (
|
<>
|
||||||
|
{terminalOutput.map((line, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`console-line ${line.type}`}
|
className={`console-line ${line.type}`}
|
||||||
>
|
>
|
||||||
{line.content}
|
{line.content}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -971,7 +1132,10 @@ int main() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button className="action-bar-btn action-save">
|
<button
|
||||||
|
className="action-bar-btn action-save"
|
||||||
|
onClick={handleSaveAndNext}
|
||||||
|
>
|
||||||
Save & Next
|
Save & Next
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<path d="M5 12h14"/>
|
<path d="M5 12h14"/>
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ const TestList = () => {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem('currentTest', JSON.stringify({
|
localStorage.setItem('currentTest', JSON.stringify({
|
||||||
id: test.id,
|
id: test.id,
|
||||||
|
title: test.title,
|
||||||
|
description: test.description,
|
||||||
|
duration_minutes: test.duration_minutes,
|
||||||
|
start_time: test.start_time,
|
||||||
|
end_time: test.end_time,
|
||||||
questions: data.questions,
|
questions: data.questions,
|
||||||
currentQuestionIndex: 0
|
currentQuestionIndex: 0
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1686,6 +1686,52 @@ body {
|
|||||||
color: #212529;
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.console-line.input {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Console Input Line - makes it look like terminal output */
|
||||||
|
.console-line.console-input-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-cursor {
|
||||||
|
color: #6c757d;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: #212529;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add blinking cursor effect */
|
||||||
|
.console-input:focus {
|
||||||
|
caret-color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
/* Action Bar */
|
/* Action Bar */
|
||||||
.action-bar {
|
.action-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user