Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions quiz/adaptiveQuiz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* adaptiveQuiz.js
* Shared helper for Adaptive Quiz Difficulty (client-side only).
*
* Usage (per quiz page JS):
* import/require is not used here (no bundler). Include this file via:
* <script src="../quiz/adaptiveQuiz.js"></script>
* Then call:
* const adaptive = buildAdaptiveQuiz({ questions });
*/

(function () {
'use strict';

function normalizeDifficulty(d) {
const val = String(d || '').toLowerCase();
if (val === 'easy' || val === 'e') return 0;
if (val === 'medium' || val === 'med' || val === 'm') return 1;
if (val === 'hard' || val === 'h') return 2;

// Default: medium
return 1;
}

function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}

/**
* Build a per-difficulty bucket of question copies.
* @param {Array<{difficulty?: string|number}>} questions
*/
function buildBuckets(questions) {
const buckets = { 0: [], 1: [], 2: [] };
questions.forEach((q, idx) => {
const di = normalizeDifficulty(q.difficulty);
// keep original index for tracking
buckets[di].push({ ...q, __qid: idx });
});

// Shuffle each bucket for variety.
Object.keys(buckets).forEach(k => {
const arr = buckets[k];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
});

return buckets;
}

function createAdaptiveQuiz({ questions, startingDifficultyIndex = 1 }) {
const buckets = buildBuckets(questions);
const totalSteps = questions.length;

let difficultyIndex = clamp(startingDifficultyIndex, 0, 2);
let consecutiveCorrect = 0;
let consecutiveIncorrect = 0;

// Flattened remaining counts for quick fallback.
function countRemainingAt(di) {
return buckets[di].length;
}

function totalRemaining() {
return buckets[0].length + buckets[1].length + buckets[2].length;
}

/**
* Pick next question from the current difficulty if possible.
* Otherwise, fallback to nearest difficulty that still has questions.
*/
function takeNext() {
if (totalRemaining() <= 0) return null;

// Try current difficulty first.
if (countRemainingAt(difficultyIndex) > 0) {
return buckets[difficultyIndex].shift();
}

// Fallback: nearest difficulty.
for (let dist = 1; dist <= 2; dist++) {
const lower = difficultyIndex - dist;
const upper = difficultyIndex + dist;
if (lower >= 0 && countRemainingAt(lower) > 0) return buckets[lower].shift();
if (upper <= 2 && countRemainingAt(upper) > 0) return buckets[upper].shift();
}

// Final fallback: any remaining.
for (let di = 0; di <= 2; di++) {
if (countRemainingAt(di) > 0) return buckets[di].shift();
}

return null;
}

/**
* Update difficulty after an answer.
*/
function updateDifficulty({ isCorrect }) {
if (isCorrect) {
consecutiveCorrect += 1;
consecutiveIncorrect = 0;

// After 2 consecutive correct: go harder.
if (consecutiveCorrect >= 2) {
difficultyIndex = clamp(difficultyIndex + 1, 0, 2);
consecutiveCorrect = 0;
}
} else {
consecutiveIncorrect += 1;
consecutiveCorrect = 0;

// After 2 consecutive incorrect: go easier.
if (consecutiveIncorrect >= 2) {
difficultyIndex = clamp(difficultyIndex - 1, 0, 2);
consecutiveIncorrect = 0;
}
}

return difficultyIndex;
}

return {
takeNext,
updateDifficulty,
getDifficultyIndex: () => difficultyIndex,
getTotalSteps: () => totalSteps,
};
}

function getStartingDifficultyFromAccuracy({ accuracy }) {
if (typeof accuracy !== 'number' || !isFinite(accuracy)) return 1; // medium
if (accuracy >= 0.8) return 2; // hard
if (accuracy <= 0.5) return 0; // easy
return 1; // medium
}

// Export to window.
window.createAdaptiveQuiz = createAdaptiveQuiz;
window.getStartingDifficultyFromAccuracy = getStartingDifficultyFromAccuracy;

})();

3 changes: 3 additions & 0 deletions quiz/motionquiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ <h2 id="result-title" tabindex="-1">🎉 Quiz Completed!</h2>
</div>

<script src="../../quizProgress.js"></script>
<script src="adaptiveQuiz.js"></script>

<script src="motionquiz.js"></script>


<script>
(function () {
if (!('serviceWorker' in navigator)) return;
Expand Down
122 changes: 97 additions & 25 deletions quiz/motionquiz.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
const questions = [
{ question: "What is the SI unit of speed?", options: ["m/s", "km/h", "m/s²", "N"], answer: "m/s" },
{ question: "What causes an object to accelerate?", options: ["Mass", "Force", "Friction", "Temperature"], answer: "Force" },
{ question: "Which of these is a scalar quantity?", options: ["Velocity", "Acceleration", "Displacement", "Speed"], answer: "Speed" },
{ question: "What does Newton's First Law state?", options: ["F = ma", "Action = Reaction", "Objects stay in motion/rest unless acted on", "Momentum is conserved"], answer: "Objects stay in motion/rest unless acted on" },
{ question: "What is the formula for acceleration?", options: ["v/t", "d/t", "Δv/t", "F/m"], answer: "Δv/t" }
{ difficulty: "easy", question: "What is the SI unit of speed?", options: ["m/s", "km/h", "m/s²", "N"], answer: "m/s" },
{ difficulty: "easy", question: "What causes an object to accelerate?", options: ["Mass", "Force", "Friction", "Temperature"], answer: "Force" },
{ difficulty: "medium", question: "Which of these is a scalar quantity?", options: ["Velocity", "Acceleration", "Displacement", "Speed"], answer: "Speed" },
{ difficulty: "medium", question: "What does Newton's First Law state?", options: ["F = ma", "Action = Reaction", "Objects stay in motion/rest unless acted on", "Momentum is conserved"], answer: "Objects stay in motion/rest unless acted on" },
{ difficulty: "hard", question: "What is the formula for acceleration?", options: ["v/t", "d/t", "Δv/t", "F/m"], answer: "Δv/t" }
];

let currentQuestionIndex = 0;
let adaptiveQuiz = null;
let adaptiveSteps = [];
let currentStepIndex = 0;
let score = 0;
let selectedOption = null;
let userAnswers = new Array(questions.length).fill(null);

let userSelectionsByStep = [];
let lastFocusedEl = null;


function getSrStatus() {
return document.getElementById("sr-status");
}
Expand All @@ -28,10 +30,16 @@ function focusMainResultHeading() {
if (heading) heading.focus();
}

function currentQuestion() {
return adaptiveSteps[currentStepIndex];
}

function loadQuestion() {
lastFocusedEl = document.activeElement;

let questionData = questions[currentQuestionIndex];
const questionData = currentQuestion();
if (!questionData) return;

document.getElementById("question").textContent = questionData.question;

let optionsContainer = document.getElementById("options");
Expand All @@ -51,18 +59,31 @@ function loadQuestion() {
optionsContainer.appendChild(btn);
});

selectedOption = null;
selectedOption = userSelectionsByStep[currentStepIndex] || null;

// Reflect selected option back into UI if navigating back.
if (selectedOption) {
document.querySelectorAll(".option").forEach(btn => {
const t = btn.textContent;
if (t === selectedOption) {
btn.classList.add("selected");
btn.setAttribute("aria-checked", "true");
}
});
}

document.getElementById("next-btn").disabled = true;
document.getElementById("prev-btn").disabled = currentQuestionIndex === 0;
document.getElementById("submit-btn").classList.toggle("hidden", currentQuestionIndex !== questions.length - 1);
document.getElementById("next-btn").classList.toggle("hidden", currentQuestionIndex === questions.length - 1);
const lastStep = adaptiveSteps.length - 1;
document.getElementById("next-btn").disabled = !selectedOption;
document.getElementById("prev-btn").disabled = currentStepIndex === 0;
document.getElementById("submit-btn").classList.toggle("hidden", currentStepIndex !== lastStep);
document.getElementById("next-btn").classList.toggle("hidden", currentStepIndex === lastStep);

updateProgressBar();

announce(`Question ${currentQuestionIndex + 1} of ${questions.length}. ${questionData.question}`);
announce(`Question ${currentStepIndex + 1} of ${adaptiveSteps.length}. ${questionData.question}`);
}


function selectOption(button, option) {
document.querySelectorAll(".option").forEach(btn => {
btn.classList.remove("selected");
Expand All @@ -73,7 +94,8 @@ function selectOption(button, option) {
button.setAttribute("aria-checked", "true");

selectedOption = option;
userAnswers[currentQuestionIndex] = option;

userSelectionsByStep[currentStepIndex] = option;

document.getElementById("next-btn").disabled = false;
announce(`Selected: ${option}`);
Expand All @@ -85,25 +107,36 @@ function nextQuestion() {
return;
}

const isCorrect = selectedOption === questions[currentQuestionIndex].answer;
const q = currentQuestion();
const isCorrect = selectedOption === q.answer;
if (isCorrect) score++;

userAnswers[currentStepIndex] = isCorrect;

announce(isCorrect ? "Correct answer." : "Incorrect answer.");

currentQuestionIndex++;
if (adaptiveQuiz) {
adaptiveQuiz.updateDifficulty({ isCorrect });
if (currentStepIndex + 1 < adaptiveSteps.length) {
adaptiveSteps[currentStepIndex + 1] = adaptiveQuiz.takeNext();
}
}

if (currentQuestionIndex < questions.length) {
currentStepIndex++;
if (currentStepIndex < adaptiveSteps.length) {
loadQuestion();
} else {
showResults();
}
}

function prevQuestion() {
currentQuestionIndex--;
currentStepIndex--;
selectedOption = userSelectionsByStep[currentStepIndex] || null;
loadQuestion();
}


function confirmSubmit() {
lastFocusedEl = document.activeElement;
document.getElementById("confirm-popup").style.display = "block";
Expand All @@ -128,16 +161,19 @@ function submitQuiz() {
}

function restartQuiz() {
currentQuestionIndex = 0;
// reset adaptive session state
currentStepIndex = 0;
score = 0;
selectedOption = null;
userAnswers.fill(null);
userAnswers = new Array(adaptiveSteps.length).fill(null);
userSelectionsByStep = new Array(adaptiveSteps.length).fill(null);

document.getElementById("quiz-box").classList.remove("hidden");
document.getElementById("result-box").classList.add("hidden");

document.getElementById("progress-bar").style.width = "0%";

startAdaptiveSession();
loadQuestion();

setTimeout(() => {
Expand All @@ -146,6 +182,7 @@ function restartQuiz() {
}, 0);
}


function showPopup() {
lastFocusedEl = document.activeElement;
document.getElementById("popup").style.display = "block";
Expand All @@ -166,7 +203,7 @@ function closeConfirmPopup() {

function showResults() {
const finishAt = Date.now();
const totalQuestions = questions.length;
const totalQuestions = adaptiveSteps.length;
const totalScore = score;
const correctCount = score;
const startAt = window.__quizMotionStartedAt || finishAt;
Expand All @@ -193,8 +230,8 @@ function showResults() {
let scoreText = `You scored <strong>${totalScore}</strong> out of ${totalQuestions}! 🎉`;
let feedbackHTML = "";

questions.forEach((q, index) => {
let userAnswer = userAnswers[index] || "No answer selected";
adaptiveSteps.forEach((q, index) => {
let userAnswer = userSelectionsByStep[index] || "No answer selected";
let isCorrect = userAnswer === q.answer;

feedbackHTML += `
Expand All @@ -213,9 +250,44 @@ function showResults() {
focusMainResultHeading();
}


function startAdaptiveSession() {
let startingDi = 1;
try {
if (window.quizProgress && typeof window.quizProgress.getTopicStats === "function") {
const stats = window.quizProgress.getTopicStats("physics-motion");
const accuracy = stats?.questionsTotal > 0 ? stats.correctTotal / stats.questionsTotal : null;
if (window.getStartingDifficultyFromAccuracy) {
startingDi = window.getStartingDifficultyFromAccuracy({ accuracy });
}
}
} catch (e) {
// ignore
}

if (!window.createAdaptiveQuiz) {
adaptiveQuiz = null;
adaptiveSteps = questions.map(q => ({ ...q }));
currentStepIndex = 0;
userAnswers = new Array(adaptiveSteps.length).fill(null);
userSelectionsByStep = new Array(adaptiveSteps.length).fill(null);
return;
}

adaptiveQuiz = window.createAdaptiveQuiz({ questions, startingDifficultyIndex: startingDi });
adaptiveSteps = new Array(questions.length).fill(null);
currentStepIndex = 0;
userAnswers = new Array(questions.length).fill(null);
userSelectionsByStep = new Array(questions.length).fill(null);

adaptiveSteps[0] = adaptiveQuiz.takeNext();
}

document.addEventListener("DOMContentLoaded", () => {
window.__quizMotionStartedAt = Date.now();
document.getElementById("progress-bar").style.width = "0%";
startAdaptiveSession();
loadQuestion();
});


2 changes: 2 additions & 0 deletions quiz/nlmquiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ <h2 id="result-title" tabindex="-1">🎉 Quiz Completed!</h2>
</div>

<script src="../../quizProgress.js"></script>
<script src="adaptiveQuiz.js"></script>
<script src="nlmquiz.js"></script>


<script>
(function () {
if (!('serviceWorker' in navigator)) return;
Expand Down
Loading
Loading