From a87484b0e73444dbb2f79fc17d4a7129f5685cb1 Mon Sep 17 00:00:00 2001 From: inubimambo Date: Tue, 8 Jul 2025 20:46:24 +0800 Subject: [PATCH] Fix 'Review Answer' button in Quiz doing nothing & improvements to quiz --- server.js | 304 ++++++++++++++++++------------------------------- views/quiz.ejs | 272 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 374 insertions(+), 202 deletions(-) diff --git a/server.js b/server.js index 9c1946d..052a317 100644 --- a/server.js +++ b/server.js @@ -1281,63 +1281,123 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => { - // Call Flowise API - const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { - question: prompt, - history: [] - }); - - - + // Call Flowise API with retry logic for better AI responses let quizData; - try { - const responseText = response.data.text || response.data.answer || response.data; + let retryCount = 0; + 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: [] + }); - - // 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]; + const responseText = response.data.text || response.data.answer || response.data; - } else { - // Try to find JSON array by counting brackets - const startIndex = responseText.indexOf('['); - if (startIndex !== -1) { - let bracketCount = 0; - let endIndex = startIndex; + // 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('{'))) { - 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 (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.'); } } - } - - if (jsonString) { + + // 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 { + // 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); - } 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('Error stack:', error.stack); - // Return fallback quiz on error - const fallbackQuiz = generateFallbackQuiz(req.body.topic || 'General Knowledge', req.body.questionCount || 5, req.body.quizType || 'multiple-choice'); + // Return error instead of fallback quiz res.json({ - success: true, - quiz: fallbackQuiz, - topic: req.body.topic || 'General Knowledge', - difficulty: req.body.difficulty || 'beginner', - questionCount: req.body.questionCount || 5, - quizType: req.body.quizType || 'multiple-choice' + success: false, + error: 'Failed to generate quiz. Please check your connection and try again, or try a different topic.' }); } }); -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) ', '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) => { try { const { answers, quiz, topic, difficulty, quizType } = req.body; diff --git a/views/quiz.ejs b/views/quiz.ejs index 4ee6392..e6a1ec9 100644 --- a/views/quiz.ejs +++ b/views/quiz.ejs @@ -143,7 +143,7 @@
-
+

Quiz Results

@@ -166,15 +166,6 @@
- -
-
-
-
Performance
-
-
-
-
@@ -339,6 +330,14 @@ document.addEventListener('DOMContentLoaded', function() { resetQuiz(); }); } + + // Add event listener for review button + const reviewBtn = document.getElementById('review-btn'); + if (reviewBtn) { + reviewBtn.addEventListener('click', () => { + reviewAnswers(); + }); + } } function displayQuestion(index) { @@ -556,6 +555,9 @@ document.addEventListener('DOMContentLoaded', function() { } function displayResults(results) { + // Store results globally for review functionality + window.lastQuizResults = results; + quizContainer.classList.add('d-none'); resultsContainer.classList.remove('d-none'); @@ -636,6 +638,256 @@ document.addEventListener('DOMContentLoaded', function() { 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 = '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 = ` +
+
+ Question ${index + 1} + ${resultForThisQuestion ? + ` + ${resultForThisQuestion.isCorrect ? 'Correct' : 'Incorrect'} + ` : '' + } +
+

${escapeHtml(question.question)}

+
+ `; + + if (question.options) { + // Multiple choice - show options with user's answer and correct answer highlighted + html += '
'; + 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 += ` +
+ + +
+ `; + }); + html += '
'; + } 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 += ` +
+ + +
+ `; + }); + } else { + // Short answer + const userAnswer = userAnswers[index] || ''; + const correctAnswer = question.correct || ''; + + html += ` +
+ +
+ ${userAnswer ? escapeHtml(userAnswer) : 'No answer provided'} +
+ +
+ ${escapeHtml(correctAnswer)} +
+
+ `; + } + + // Add explanation if available + if (resultForThisQuestion && resultForThisQuestion.explanation) { + html += ` +
+ Explanation: +

${escapeHtml(resultForThisQuestion.explanation)}

+
+ `; + } + + 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 = '
'; + + 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 += ` +
+ +
+ `; + }); + + html += '
'; + overviewDiv.innerHTML = html; + } + + window.goToQuestionInReview = function(index) { + currentQuestion = index; + displayQuestionInReviewMode(currentQuestion); + }; });