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.

+
+
🧠 0 missed
+
📚
+
+
+ +
+

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)
+
+
+
+ +
+
Best Score
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
Filtering quiz attempts from localStorage.