Improvements to quiz and dashboards. #9
304
server.js
304
server.js
@@ -1281,63 +1281,123 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Call Flowise API
|
// Call Flowise API with retry logic for better AI responses
|
||||||
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
|
|
||||||
question: prompt,
|
|
||||||
history: []
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let quizData;
|
let quizData;
|
||||||
try {
|
let retryCount = 0;
|
||||||
const responseText = response.data.text || response.data.answer || response.data;
|
const maxRetries = 2;
|
||||||
|
|
||||||
|
while (retryCount <= maxRetries) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
|
||||||
|
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: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = response.data.text || response.data.answer || response.data;
|
||||||
// Try to extract JSON from the response
|
|
||||||
let jsonString = null;
|
|
||||||
|
|
||||||
// First, try to find JSON wrapped in code blocks
|
|
||||||
const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
||||||
if (codeBlockMatch) {
|
|
||||||
jsonString = codeBlockMatch[1];
|
|
||||||
|
|
||||||
} else {
|
// Check if the AI response looks like it's not following instructions
|
||||||
// Try to find JSON array by counting brackets
|
if (responseText.toLowerCase().includes('do you have a question') ||
|
||||||
const startIndex = responseText.indexOf('[');
|
responseText.toLowerCase().includes('would you like me to help') ||
|
||||||
if (startIndex !== -1) {
|
responseText.toLowerCase().includes('something else related') ||
|
||||||
let bracketCount = 0;
|
responseText.toLowerCase().includes('how can i help') ||
|
||||||
let endIndex = startIndex;
|
(!responseText.includes('[') && !responseText.includes('{'))) {
|
||||||
|
|
||||||
for (let i = startIndex; i < responseText.length; i++) {
|
if (retryCount < maxRetries) {
|
||||||
if (responseText[i] === '[') bracketCount++;
|
retryCount++;
|
||||||
if (responseText[i] === ']') bracketCount--;
|
console.log(`AI gave improper response, retrying... (attempt ${retryCount + 1})`);
|
||||||
if (bracketCount === 0) {
|
continue;
|
||||||
endIndex = i;
|
} else {
|
||||||
break;
|
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.');
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bracketCount === 0) {
|
|
||||||
jsonString = responseText.substring(startIndex, endIndex + 1);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Try to extract JSON from the response
|
||||||
if (jsonString) {
|
let jsonString = null;
|
||||||
|
|
||||||
|
// First, try to find JSON wrapped in code blocks
|
||||||
|
const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
||||||
|
if (codeBlockMatch) {
|
||||||
|
jsonString = codeBlockMatch[1];
|
||||||
|
} else {
|
||||||
|
// Try to find JSON array by counting brackets
|
||||||
|
const startIndex = responseText.indexOf('[');
|
||||||
|
if (startIndex !== -1) {
|
||||||
|
let bracketCount = 0;
|
||||||
|
let endIndex = startIndex;
|
||||||
|
|
||||||
|
for (let i = startIndex; i < responseText.length; i++) {
|
||||||
|
if (responseText[i] === '[') bracketCount++;
|
||||||
|
if (responseText[i] === ']') bracketCount--;
|
||||||
|
if (bracketCount === 0) {
|
||||||
|
endIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bracketCount === 0) {
|
||||||
|
jsonString = responseText.substring(startIndex, endIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonString) {
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
retryCount++;
|
||||||
|
console.log(`Could not find JSON in response, retrying... (attempt ${retryCount + 1})`);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find valid JSON in the AI response after multiple attempts. Please try generating the quiz again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
quizData = JSON.parse(jsonString);
|
quizData = JSON.parse(jsonString);
|
||||||
} else {
|
|
||||||
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.'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
|
||||||
console.error('Quiz parsing error:', parseError);
|
|
||||||
quizData = generateFallbackQuiz(topic, questionCount, quizType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure quizData is always defined and is an array
|
|
||||||
if (!quizData || !Array.isArray(quizData) || quizData.length === 0) {
|
|
||||||
quizData = generateFallbackQuiz(topic, questionCount, quizType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
272
views/quiz.ejs
272
views/quiz.ejs
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user