diff --git a/dashboardProgress.js b/dashboardProgress.js
index bd8f467..1d683bf 100644
--- a/dashboardProgress.js
+++ b/dashboardProgress.js
@@ -11,6 +11,13 @@
*/
(function () {
+ /**
+ * Progress Analytics Dashboard (extended)
+ * - Rolling average score + best score (from attempts history)
+ * - Mastery per category (subject)
+ * - Time spent per quiz (from attempts history)
+ * - Filters: date range + subject
+ */
function pct(n) {
if (typeof n !== "number" || Number.isNaN(n)) return "—";
return `${Math.round(n * 100)}%`;
diff --git a/progress.js b/progress.js
index 53fd6a1..8918d57 100644
--- a/progress.js
+++ b/progress.js
@@ -40,18 +40,30 @@ const STATE_COLORS = {
};
const STORAGE_KEY = "learnsphere_progress";
+// XP system constants
+const XP_PER_LEVEL = 1000; // XP required per level
const REVIEW_SCHEDULE_KEY = "learnsphere_review_schedule_v1";
// ── Storage Helpers ───────────────────────────────────────────────────────────
function loadProgress() {
try {
- return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
+ const data = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
+ // Ensure XP and level fields exist
+ if (typeof data.xp !== "number") data.xp = 0;
+ if (typeof data.level !== "number") data.level = 0;
+ return data;
} catch {
- return {};
+ return { xp: 0, level: 0 };
}
}
+// Helper to calculate level from XP
+function calculateLevel(xp) {
+ if (typeof xp !== "number" || xp < 0) return 0;
+ return Math.floor(xp / XP_PER_LEVEL);
+}
+
function loadReviewSchedule() {
try {
return JSON.parse(localStorage.getItem(REVIEW_SCHEDULE_KEY)) || {};
@@ -71,6 +83,9 @@ function saveReviewSchedule(scheduleMap) {
function saveProgress(progressMap) {
try {
+ // Ensure XP and level are persisted
+ if (typeof progressMap.xp !== "number") progressMap.xp = 0;
+ if (typeof progressMap.level !== "number") progressMap.level = 0;
localStorage.setItem(STORAGE_KEY, JSON.stringify(progressMap));
} catch (e) {
console.warn("LearnSphere: Could not save progress to localStorage.", e);
@@ -281,6 +296,17 @@ function updateProgressSummary() {
window.studyProgress = {
STREAK_KEY: "learnsphere_streak_state_v1",
+ // XP related helpers
+ addXP(amount) {
+ const progress = loadProgress();
+ const inc = Number(amount) || 0;
+ progress.xp = (progress.xp || 0) + inc;
+ progress.level = calculateLevel(progress.xp);
+ saveProgress(progress);
+ },
+ getXP() { return (loadProgress().xp) || 0; },
+ getLevel() { return (loadProgress().level) || 0; },
+ XP_PER_LEVEL,
loadStreakState() {
try {
diff --git a/quiz/bank/physics-motion.json b/quiz/bank/physics-motion.json
new file mode 100644
index 0000000..89a0756
--- /dev/null
+++ b/quiz/bank/physics-motion.json
@@ -0,0 +1,37 @@
+[
+ {
+ "category": "General",
+ "difficulty": "easy",
+ "question": "What is the SI unit of speed?",
+ "options": ["m/s", "km/h", "m/s²", "N"],
+ "answer": "m/s"
+ },
+ {
+ "category": "General",
+ "difficulty": "easy",
+ "question": "What causes an object to accelerate?",
+ "options": ["Mass", "Force", "Friction", "Temperature"],
+ "answer": "Force"
+ },
+ {
+ "category": "General",
+ "difficulty": "medium",
+ "question": "Which of these is a scalar quantity?",
+ "options": ["Velocity", "Acceleration", "Displacement", "Speed"],
+ "answer": "Speed"
+ },
+ {
+ "category": "General",
+ "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"
+ },
+ {
+ "category": "General",
+ "difficulty": "hard",
+ "question": "What is the formula for acceleration?",
+ "options": ["v/t", "d/t", "Δv/t", "F/m"],
+ "answer": "Δv/t"
+ }
+]
diff --git a/quizProgress.js b/quizProgress.js
index 7058db8..3701f2a 100644
--- a/quizProgress.js
+++ b/quizProgress.js
@@ -660,6 +660,13 @@ function _updateUnifiedStreakAndGoal(type, value = 1) {
}
}
+function getAttemptsHistory() {
+ const state = _loadState();
+ const attempts = Array.isArray(state?.attempts) ? state.attempts : [];
+ // Return a copy to avoid accidental mutation
+ return attempts.slice();
+}
+
window.quizProgress = {
QUIZ_TOPICS,
SKILL_TAXONOMY,
@@ -674,6 +681,7 @@ window.quizProgress = {
getWeakestSkills,
getQuestionWeaknessWeight,
recordRetryAttempt,
+ getAttemptsHistory,
};
diff --git a/review_mistakes.html b/review_mistakes.html
new file mode 100644
index 0000000..11fdd16
--- /dev/null
+++ b/review_mistakes.html
@@ -0,0 +1,268 @@
+
+
+
+
+
+
+ Review Mistakes – LearnSphere
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Review Mistakes
+
See what you missed and retry only those questions.
+
+
+
+
+
Missed questions
+
+
+ 🔎
+ No missed questions found for this topic.
+
+
+
+
+ After retrying, spaced repetition + retry mastery stats will be updated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/review_mistakes.js b/review_mistakes.js
new file mode 100644
index 0000000..0861f2a
--- /dev/null
+++ b/review_mistakes.js
@@ -0,0 +1,157 @@
+(function () {
+ const MISSED_KEY = "learnsphere_review_missed_v1";
+
+ function _todayLocalISODate() {
+ const d = new Date();
+ const yyyy = d.getFullYear();
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
+ const dd = String(d.getDate()).padStart(2, "0");
+ return `${yyyy}-${mm}-${dd}`;
+ }
+
+ function _safeParse(json, fallback) {
+ try {
+ return JSON.parse(json) || fallback;
+ } catch {
+ return fallback;
+ }
+ }
+
+ function loadMissedMap() {
+ return _safeParse(localStorage.getItem(MISSED_KEY), {});
+ }
+
+ function saveMissedMap(m) {
+ try {
+ localStorage.setItem(MISSED_KEY, JSON.stringify(m));
+ } catch (e) {
+ console.warn("LearnSphere: could not save missed map", e);
+ }
+ }
+
+ function getTopicIdFromQuery() {
+ const urlParams = new URLSearchParams(window.location.search);
+ return urlParams.get("topic") || urlParams.get("topicId") || null;
+ }
+
+ function buildExplainText(question) {
+ if (!question) return "";
+ if (typeof question.explanation === "string" && question.explanation.trim()) return question.explanation.trim();
+ if (typeof question.explanation === "function") return question.explanation();
+ return "";
+ }
+
+ function getReviewQuizBankForTopic(topicId) {
+ if (window.ReviewMode && typeof window.ReviewMode.__getReviewQuizBank === "function") {
+ return window.ReviewMode.__getReviewQuizBank(topicId);
+ }
+ return null;
+ }
+
+ function getMissedQids(topicId) {
+ const map = loadMissedMap();
+ const entry = map[topicId];
+ const qids = entry && Array.isArray(entry.missedQids) ? entry.missedQids : [];
+ return qids;
+ }
+
+ function setMissedQids(topicId, missedQids) {
+ const map = loadMissedMap();
+ map[topicId] = {
+ ...(map[topicId] || {}),
+ missedQids: Array.isArray(missedQids) ? missedQids : [],
+ updatedAt: Date.now(),
+ };
+ saveMissedMap(map);
+ }
+
+ function renderList({ topicId, missedQids }) {
+ const listEl = document.getElementById("rm-list");
+ const totalEl = document.getElementById("rm-total-missed");
+ const topicLabelEl = document.getElementById("rm-topic-label");
+
+ if (totalEl) totalEl.textContent = String(missedQids.length);
+ if (topicLabelEl) topicLabelEl.textContent = topicId || "—";
+
+ if (!listEl) return;
+ listEl.innerHTML = "";
+
+ if (!topicId || missedQids.length === 0) {
+ listEl.innerHTML = `
+
+ 🔎
+ No missed questions found for this topic.
+
+ `;
+ return;
+ }
+
+ const quizBank = getReviewQuizBankForTopic(topicId) || [];
+
+ // quizBank items expected to have __qid
+ const byQid = new Map();
+ quizBank.forEach((q) => {
+ if (q && typeof q.__qid === "string") byQid.set(q.__qid, q);
+ });
+
+ missedQids.forEach((qid, idx) => {
+ const q = byQid.get(qid);
+ const qText = q?.q || "(Question text unavailable)";
+ const correctIndex = typeof q?.answerIndex === "number" ? q.answerIndex : null;
+ const correctText =
+ correctIndex !== null && Array.isArray(q?.options) ? q.options[correctIndex] : "";
+
+ const item = document.createElement("div");
+ item.className = "review-mistake-item";
+
+ item.innerHTML = `
+ ${idx + 1}. ${qText}
+
+ Correct answer: ${correctText || "—"}
+
+ ${q?.explanation ? `${q.explanation}
` : ``}
+ `;
+
+ listEl.appendChild(item);
+ });
+ }
+
+ function initRetryMode({ topicId }) {
+ const retryBtn = document.getElementById("rm-retryBtn");
+ const retryBtnEnabled = !!retryBtn;
+
+ if (!retryBtnEnabled) return;
+
+ const missedQids = getMissedQids(topicId);
+ retryBtn.disabled = missedQids.length === 0;
+
+ retryBtn.addEventListener("click", () => {
+ if (!window.ReviewMode || typeof window.ReviewMode.start !== "function") return;
+
+ // Start the review modal in retry-only mode.
+ // We pass retryQids; ReviewMode.start will render only missed questions.
+ window.ReviewMode.start(topicId, { retryQids: missedQids });
+ });
+ }
+
+ document.addEventListener("DOMContentLoaded", () => {
+ const topicId = getTopicIdFromQuery();
+
+ const missedQids = getMissedQids(topicId);
+
+ renderList({ topicId, missedQids });
+ initRetryMode({ topicId });
+
+ const retryBtn = document.getElementById("rm-retryBtn");
+ if (retryBtn && missedQids.length > 0) retryBtn.disabled = false;
+
+ // Listen for retry completion: ReviewMode should clear missed after successful retry.
+ window.addEventListener("review-mistakes-retried", () => {
+ const latestMissed = getMissedQids(topicId);
+ renderList({ topicId, missedQids: latestMissed });
+ initRetryMode({ topicId });
+ });
+ });
+
+})();
+
diff --git a/teachers.html b/teachers.html
index ff576a9..34daea8 100644
--- a/teachers.html
+++ b/teachers.html
@@ -82,7 +82,7 @@ Teacher Progress Dashboard
-
+
Current Streak
—
@@ -98,6 +98,46 @@
Teacher Progress Dashboard
—
—
+
+
+
Rolling Avg (7d)
+
—
+
—
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Filtering quiz attempts from localStorage.