Fix 'Review Answer' button in Quiz doing nothing & improvements to quiz

This commit is contained in:
inubimambo
2025-07-08 20:46:24 +08:00
parent 615d7e3352
commit a87484b0e7
2 changed files with 374 additions and 202 deletions

240
server.js
View File

@@ -1281,18 +1281,37 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => {
// Call Flowise API // Call Flowise API with retry logic for better AI responses
let quizData;
let retryCount = 0;
const maxRetries = 2;
while (retryCount <= maxRetries) {
try {
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
question: prompt, question: retryCount > 0 ?
`${prompt}\n\nIMPORTANT: Please provide ONLY a valid JSON array of questions. Do not include any explanatory text, greetings, or additional commentary. Start your response directly with [ and end with ].` :
prompt,
history: [] history: []
}); });
let quizData;
try {
const responseText = response.data.text || response.data.answer || response.data; const responseText = response.data.text || response.data.answer || response.data;
// Check if the AI response looks like it's not following instructions
if (responseText.toLowerCase().includes('do you have a question') ||
responseText.toLowerCase().includes('would you like me to help') ||
responseText.toLowerCase().includes('something else related') ||
responseText.toLowerCase().includes('how can i help') ||
(!responseText.includes('[') && !responseText.includes('{'))) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`AI gave improper response, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error('The AI is not responding properly to quiz generation requests. Please try again with a more specific topic or try again later.');
}
}
// Try to extract JSON from the response // Try to extract JSON from the response
let jsonString = null; let jsonString = null;
@@ -1301,7 +1320,6 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => {
const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/); const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
if (codeBlockMatch) { if (codeBlockMatch) {
jsonString = codeBlockMatch[1]; jsonString = codeBlockMatch[1];
} else { } else {
// Try to find JSON array by counting brackets // Try to find JSON array by counting brackets
const startIndex = responseText.indexOf('['); const startIndex = responseText.indexOf('[');
@@ -1320,24 +1338,66 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => {
if (bracketCount === 0) { if (bracketCount === 0) {
jsonString = responseText.substring(startIndex, endIndex + 1); jsonString = responseText.substring(startIndex, endIndex + 1);
} }
} }
} }
if (jsonString) { if (!jsonString) {
quizData = JSON.parse(jsonString); if (retryCount < maxRetries) {
retryCount++;
console.log(`Could not find JSON in response, retrying... (attempt ${retryCount + 1})`);
continue;
} else { } else {
quizData = generateFallbackQuiz(topic, questionCount, quizType); throw new Error('Could not find valid JSON in the AI response after multiple attempts. Please try generating the quiz again.');
} }
} catch (parseError) {
console.error('Quiz parsing error:', parseError);
quizData = generateFallbackQuiz(topic, questionCount, quizType);
} }
// Ensure quizData is always defined and is an array quizData = JSON.parse(jsonString);
if (!quizData || !Array.isArray(quizData) || quizData.length === 0) {
quizData = generateFallbackQuiz(topic, questionCount, quizType); // Validate the parsed data
if (!Array.isArray(quizData) || quizData.length === 0) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Invalid quiz format received, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error('The AI generated an invalid quiz format after multiple attempts. Please try again.');
}
}
// Validate each question has required fields
for (let i = 0; i < quizData.length; i++) {
const q = quizData[i];
if (!q.question || (!q.correct && !q.answer)) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Question ${i + 1} missing required fields, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error(`Question ${i + 1} is missing required fields after multiple attempts. Please try generating the quiz again.`);
}
}
}
// If we get here, the quiz is valid
break;
} catch (parseError) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Parse error occurred, retrying... (attempt ${retryCount + 1}):`, parseError.message);
continue;
} else {
console.error('Quiz parsing error after retries:', parseError);
console.error('AI Response was:', response?.data?.text || response?.data?.answer || response?.data || 'No response');
// Return error instead of fallback
return res.json({
success: false,
error: parseError.message || 'Failed to parse quiz questions from AI response after multiple attempts. Please try again with a different topic or rephrase your request.'
});
}
}
} }
@@ -1354,154 +1414,14 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => {
console.error('Quiz generation error:', error); console.error('Quiz generation error:', error);
console.error('Error stack:', error.stack); console.error('Error stack:', error.stack);
// Return fallback quiz on error // Return error instead of fallback quiz
const fallbackQuiz = generateFallbackQuiz(req.body.topic || 'General Knowledge', req.body.questionCount || 5, req.body.quizType || 'multiple-choice');
res.json({ res.json({
success: true, success: false,
quiz: fallbackQuiz, error: 'Failed to generate quiz. Please check your connection and try again, or try a different topic.'
topic: req.body.topic || 'General Knowledge',
difficulty: req.body.difficulty || 'beginner',
questionCount: req.body.questionCount || 5,
quizType: req.body.quizType || 'multiple-choice'
}); });
} }
}); });
function generateFallbackQuiz(topic, questionCount, quizType) {
const questions = [];
// Generate actual questions based on topic
const topicQuestions = {
'javascript': [
{
question: 'What is the correct way to declare a variable in JavaScript?',
options: ['A) var myVar = 5;', 'B) variable myVar = 5;', 'C) declare myVar = 5;', 'D) int myVar = 5;'],
correct: 'A',
explanation: 'The var keyword is used to declare variables in JavaScript.'
},
{
question: 'Which method is used to add an element to the end of an array?',
options: ['A) push()', 'B) add()', 'C) append()', 'D) insert()'],
correct: 'A',
explanation: 'The push() method adds one or more elements to the end of an array.'
},
{
question: 'What does === operator do in JavaScript?',
options: ['A) Assigns a value', 'B) Compares values only', 'C) Compares values and types', 'D) Declares a constant'],
correct: 'C',
explanation: 'The === operator compares both value and type without type conversion.'
},
{
question: 'How do you write a comment in JavaScript?',
options: ['A) <!-- comment -->', 'B) // comment', 'C) # comment', 'D) /* comment */'],
correct: 'B',
explanation: 'Single-line comments in JavaScript start with //.'
},
{
question: 'What is the result of typeof null in JavaScript?',
options: ['A) "null"', 'B) "undefined"', 'C) "object"', 'D) "boolean"'],
correct: 'C',
explanation: 'typeof null returns "object" due to a legacy bug in JavaScript.'
}
],
'python': [
{
question: 'Which keyword is used to create a function in Python?',
options: ['A) function', 'B) def', 'C) create', 'D) func'],
correct: 'B',
explanation: 'The def keyword is used to define functions in Python.'
},
{
question: 'What is the correct way to create a list in Python?',
options: ['A) list = (1, 2, 3)', 'B) list = {1, 2, 3}', 'C) list = [1, 2, 3]', 'D) list = <1, 2, 3>'],
correct: 'C',
explanation: 'Square brackets [] are used to create lists in Python.'
},
{
question: 'How do you start a comment in Python?',
options: ['A) //', 'B) #', 'C) /*', 'D) --'],
correct: 'B',
explanation: 'Comments in Python start with the # symbol.'
},
{
question: 'What does len() function do in Python?',
options: ['A) Returns the length of an object', 'B) Converts to lowercase', 'C) Rounds a number', 'D) Prints output'],
correct: 'A',
explanation: 'The len() function returns the number of items in an object.'
},
{
question: 'Which of the following is a mutable data type in Python?',
options: ['A) tuple', 'B) string', 'C) list', 'D) integer'],
correct: 'C',
explanation: 'Lists are mutable, meaning they can be changed after creation.'
}
],
'math': [
{
question: 'What is the value of π (pi) approximately?',
options: ['A) 3.14159', 'B) 2.71828', 'C) 1.61803', 'D) 4.66920'],
correct: 'A',
explanation: 'π (pi) is approximately 3.14159, the ratio of circumference to diameter.'
},
{
question: 'What is the derivative of x²?',
options: ['A) x', 'B) 2x', 'C) x³', 'D) 2x²'],
correct: 'B',
explanation: 'Using the power rule, the derivative of x² is 2x.'
},
{
question: 'What is the Pythagorean theorem?',
options: ['A) a + b = c', 'B) a² + b² = c²', 'C) a × b = c', 'D) a² - b² = c²'],
correct: 'B',
explanation: 'The Pythagorean theorem states that a² + b² = c² for right triangles.'
},
{
question: 'What is the factorial of 5?',
options: ['A) 25', 'B) 120', 'C) 60', 'D) 100'],
correct: 'B',
explanation: '5! = 5 × 4 × 3 × 2 × 1 = 120'
},
{
question: 'What is the square root of 64?',
options: ['A) 6', 'B) 7', 'C) 8', 'D) 9'],
correct: 'C',
explanation: 'The square root of 64 is 8 because 8² = 64.'
}
]
};
// Get appropriate questions for the topic
const availableQuestions = topicQuestions[topic.toLowerCase()] || topicQuestions['math'];
for (let i = 0; i < questionCount; i++) {
const questionIndex = i % availableQuestions.length;
const baseQuestion = availableQuestions[questionIndex];
if (quizType === 'multiple-choice') {
questions.push({
question: baseQuestion.question,
options: baseQuestion.options,
correct: baseQuestion.correct,
explanation: baseQuestion.explanation
});
} else if (quizType === 'true-false') {
questions.push({
question: baseQuestion.question.replace(/Which|What|How/, 'Is it true that'),
correct: Math.random() > 0.5 ? 'True' : 'False',
explanation: baseQuestion.explanation
});
} else {
questions.push({
question: baseQuestion.question,
answer: baseQuestion.correct,
keywords: [baseQuestion.correct.toLowerCase(), topic.toLowerCase()]
});
}
}
return questions;
}
app.post('/api/submit-quiz', requireAuth, async (req, res) => { app.post('/api/submit-quiz', requireAuth, async (req, res) => {
try { try {
const { answers, quiz, topic, difficulty, quizType } = req.body; const { answers, quiz, topic, difficulty, quizType } = req.body;

View File

@@ -143,7 +143,7 @@
<!-- Results Container --> <!-- Results Container -->
<div id="results-container" class="d-none"> <div id="results-container" class="d-none">
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-12">
<div class="card shadow-sm border-0"> <div class="card shadow-sm border-0">
<div class="card-header bg-info text-white"> <div class="card-header bg-info text-white">
<h4 class="mb-0"><i class="fas fa-chart-line me-2"></i>Quiz Results</h4> <h4 class="mb-0"><i class="fas fa-chart-line me-2"></i>Quiz Results</h4>
@@ -166,15 +166,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5><i class="fas fa-trophy me-2"></i>Performance</h5>
<div id="performance-chart"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -339,6 +330,14 @@ document.addEventListener('DOMContentLoaded', function() {
resetQuiz(); resetQuiz();
}); });
} }
// Add event listener for review button
const reviewBtn = document.getElementById('review-btn');
if (reviewBtn) {
reviewBtn.addEventListener('click', () => {
reviewAnswers();
});
}
} }
function displayQuestion(index) { function displayQuestion(index) {
@@ -556,6 +555,9 @@ document.addEventListener('DOMContentLoaded', function() {
} }
function displayResults(results) { function displayResults(results) {
// Store results globally for review functionality
window.lastQuizResults = results;
quizContainer.classList.add('d-none'); quizContainer.classList.add('d-none');
resultsContainer.classList.remove('d-none'); resultsContainer.classList.remove('d-none');
@@ -636,6 +638,256 @@ document.addEventListener('DOMContentLoaded', function() {
quizForm.reset(); quizForm.reset();
} }
function reviewAnswers() {
// Switch from results view back to quiz view for review
resultsContainer.classList.add('d-none');
quizContainer.classList.remove('d-none');
// Set to review mode and start from first question
currentQuestion = 0;
displayQuestionInReviewMode(currentQuestion);
displayOverviewInReviewMode();
// Update navigation for review mode
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
// Remove existing event listeners by cloning elements
if (prevBtn) {
const newPrevBtn = prevBtn.cloneNode(true);
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
newPrevBtn.onclick = () => {
if (currentQuestion > 0) {
currentQuestion--;
displayQuestionInReviewMode(currentQuestion);
}
};
}
if (nextBtn) {
const newNextBtn = nextBtn.cloneNode(true);
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
newNextBtn.onclick = () => {
if (currentQuestion < currentQuiz.length - 1) {
currentQuestion++;
displayQuestionInReviewMode(currentQuestion);
}
};
}
// Hide submit button completely in review mode
if (submitBtn) {
submitBtn.classList.add('d-none');
}
// Add a "Back to Results" button
const cardBody = submitBtn.parentElement;
let backToResultsBtn = document.getElementById('back-to-results-btn');
if (!backToResultsBtn) {
backToResultsBtn = document.createElement('button');
backToResultsBtn.id = 'back-to-results-btn';
backToResultsBtn.className = 'btn btn-info';
backToResultsBtn.innerHTML = '<i class="fas fa-arrow-left me-2"></i>Back to Results';
backToResultsBtn.onclick = () => {
quizContainer.classList.add('d-none');
resultsContainer.classList.remove('d-none');
// Remove the back button
backToResultsBtn.remove();
};
cardBody.appendChild(backToResultsBtn);
}
}
function displayQuestionInReviewMode(index) {
if (!currentQuiz || index >= currentQuiz.length) {
console.error('Invalid question index or quiz not loaded');
return;
}
const question = currentQuiz[index];
const questionsDiv = document.getElementById('quiz-questions');
if (!questionsDiv) {
console.error('Questions div not found!');
return;
}
// Get the quiz results from the last submission
const resultForThisQuestion = window.lastQuizResults ?
window.lastQuizResults.results[index] : null;
let html = `
<div class="mb-4">
<h5 class="mb-3">
Question ${index + 1}
${resultForThisQuestion ?
`<span class="badge ${resultForThisQuestion.isCorrect ? 'bg-success' : 'bg-danger'} ms-2">
${resultForThisQuestion.isCorrect ? 'Correct' : 'Incorrect'}
</span>` : ''
}
</h5>
<p class="lead">${escapeHtml(question.question)}</p>
</div>
`;
if (question.options) {
// Multiple choice - show options with user's answer and correct answer highlighted
html += '<div class="mb-3">';
question.options.forEach((option, i) => {
const optionLetter = option.charAt(0);
const userSelected = userAnswers[index] === optionLetter;
const isCorrect = question.correct === optionLetter;
let className = 'form-check mb-2';
let labelClass = 'form-check-label';
if (userSelected && isCorrect) {
className += ' bg-success bg-opacity-10 border border-success rounded p-2';
labelClass += ' text-success fw-bold';
} else if (userSelected && !isCorrect) {
className += ' bg-danger bg-opacity-10 border border-danger rounded p-2';
labelClass += ' text-danger fw-bold';
} else if (!userSelected && isCorrect) {
className += ' bg-warning bg-opacity-10 border border-warning rounded p-2';
labelClass += ' text-warning fw-bold';
}
html += `
<div class="${className}">
<input class="form-check-input" type="radio" disabled
${userSelected ? 'checked' : ''}>
<label class="${labelClass}">
${escapeHtml(option)}
${isCorrect ? ' ✓ (Correct answer)' : ''}
</label>
</div>
`;
});
html += '</div>';
} else if (question.correct === 'True' || question.correct === 'False') {
// True/False
const userSelected = userAnswers[index];
const correctAnswer = question.correct;
['True', 'False'].forEach(option => {
const userSelectedThis = userSelected === option;
const isCorrect = correctAnswer === option;
let className = 'form-check mb-2';
let labelClass = 'form-check-label';
if (userSelectedThis && isCorrect) {
className += ' bg-success bg-opacity-10 border border-success rounded p-2';
labelClass += ' text-success fw-bold';
} else if (userSelectedThis && !isCorrect) {
className += ' bg-danger bg-opacity-10 border border-danger rounded p-2';
labelClass += ' text-danger fw-bold';
} else if (!userSelectedThis && isCorrect) {
className += ' bg-warning bg-opacity-10 border border-warning rounded p-2';
labelClass += ' text-warning fw-bold';
}
html += `
<div class="${className}">
<input class="form-check-input" type="radio" disabled
${userSelectedThis ? 'checked' : ''}>
<label class="${labelClass}">
${option}
${isCorrect ? ' ✓ (Correct answer)' : ''}
</label>
</div>
`;
});
} else {
// Short answer
const userAnswer = userAnswers[index] || '';
const correctAnswer = question.correct || '';
html += `
<div class="mb-3">
<label class="form-label fw-bold">Your Answer:</label>
<div class="p-3 bg-light border rounded">
${userAnswer ? escapeHtml(userAnswer) : '<em>No answer provided</em>'}
</div>
<label class="form-label fw-bold mt-3 text-success">Correct Answer:</label>
<div class="p-3 bg-success bg-opacity-10 border border-success rounded">
${escapeHtml(correctAnswer)}
</div>
</div>
`;
}
// Add explanation if available
if (resultForThisQuestion && resultForThisQuestion.explanation) {
html += `
<div class="mt-3 p-3 bg-info bg-opacity-10 border border-info rounded">
<strong>Explanation:</strong>
<p class="mb-0 mt-2">${escapeHtml(resultForThisQuestion.explanation)}</p>
</div>
`;
}
questionsDiv.innerHTML = html;
// Update progress
const progressEl = document.getElementById('quiz-progress');
if (progressEl) {
progressEl.textContent = `Review: Question ${index + 1} of ${currentQuiz.length}`;
}
// Update navigation buttons for review mode
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
if (prevBtn) prevBtn.disabled = index === 0;
if (nextBtn) {
nextBtn.disabled = index === currentQuiz.length - 1;
nextBtn.classList.remove('d-none');
}
// Keep submit button hidden in review mode
if (submitBtn) {
submitBtn.classList.add('d-none');
}
}
function displayOverviewInReviewMode() {
const overviewDiv = document.getElementById('quiz-overview');
let html = '<div class="row">';
currentQuiz.forEach((_, i) => {
const resultForQuestion = window.lastQuizResults ?
window.lastQuizResults.results[i] : null;
const isCurrent = i === currentQuestion;
let btnClass = 'btn btn-sm w-100 ';
if (isCurrent) {
btnClass += 'btn-primary';
} else if (resultForQuestion) {
btnClass += resultForQuestion.isCorrect ? 'btn-success' : 'btn-danger';
} else {
btnClass += 'btn-outline-secondary';
}
html += `
<div class="col-4 mb-2">
<button class="${btnClass}" onclick="goToQuestionInReview(${i})">
${i + 1}
</button>
</div>
`;
});
html += '</div>';
overviewDiv.innerHTML = html;
}
window.goToQuestionInReview = function(index) {
currentQuestion = index;
displayQuestionInReviewMode(currentQuestion);
};
}); });
</script> </script>