diff --git a/badges.js b/badges.js index 8e731e9..20c8205 100644 --- a/badges.js +++ b/badges.js @@ -131,9 +131,23 @@ progressText }; } + }, + { + id: "daily_goal_hero", + title: "Daily Goal Hero", + description: "Complete your daily learning goal today.", + icon: "π₯", + getProgress: (stats) => { + const unlocked = stats.dailyGoalCompleted || false; + return { + unlocked, + progressText: unlocked ? "Unlocked" : "0/1" + }; + } } ]; + function loadAchievements() { try { const raw = localStorage.getItem(ACHIEVEMENTS_KEY); @@ -182,7 +196,40 @@ } // Streak - const streak = window.quizProgress.getStreak ? window.quizProgress.getStreak() : { currentStreak: 0 }; + let currentStreak = 0; + if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") { + currentStreak = window.studyProgress.loadStreakState().currentStreak || 0; + } else if (window.quizProgress && typeof window.quizProgress.getStreak === "function") { + const streak = window.quizProgress.getStreak(); + currentStreak = streak.currentStreak || 0; + } + + // Daily Goal + let dailyGoalCompleted = false; + if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") { + const state = window.studyProgress.loadStreakState(); + const qDone = state.dailyGoalProgress.quizzesCompleted || 0; + const rDone = state.dailyGoalProgress.questionsReviewed || 0; + if (qDone >= 1 || rDone >= 10) { + dailyGoalCompleted = true; + } + } else { + const STREAK_KEY = "learnsphere_streak_state_v1"; + try { + const raw = localStorage.getItem(STREAK_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && parsed.dailyGoalProgress) { + const qDone = parsed.dailyGoalProgress.quizzesCompleted || 0; + const rDone = parsed.dailyGoalProgress.questionsReviewed || 0; + if (qDone >= 1 || rDone >= 10) { + dailyGoalCompleted = true; + } + } + } + } catch (e) {} + } + // Overall accuracy const overall = window.quizProgress.getOverallAccuracy ? window.quizProgress.getOverallAccuracy() : { accuracy: null, total: 0 }; @@ -230,7 +277,8 @@ return { attemptCount, topicAttemptedCount, - currentStreak: safeNumber(streak?.currentStreak) ?? 0, + currentStreak: safeNumber(currentStreak) ?? 0, + dailyGoalCompleted, overallAccuracy: overall?.accuracy == null ? null : safeNumber(overall.accuracy), overallTotalAnswers: safeNumber(overall?.total) ?? 0, hasWeekendAttempt, @@ -238,6 +286,7 @@ }; } + function ensureToastStyles() { if (document.getElementById("badge-toast-styles")) return; const style = document.createElement("style"); diff --git a/dashboardProgress.js b/dashboardProgress.js index 243fc06..bd8f467 100644 --- a/dashboardProgress.js +++ b/dashboardProgress.js @@ -115,18 +115,44 @@ } function renderKpis(root) { - const streak = window.quizProgress?.getStreak?.(); - const overall = window.quizProgress?.getOverallAccuracy?.(); + let currentStreak = 0; + let streakMetaText = "No practice yet."; + let dailyGoalPct = 0; + let dailyGoalMetaText = "Complete 1 quiz or review 10 questions."; + + if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") { + const streakState = window.studyProgress.loadStreakState(); + currentStreak = streakState.currentStreak || 0; + const last = streakState.lastActiveDate; + streakMetaText = last ? `Last active: ${last}` : "No activity yet."; + + const qDone = streakState.dailyGoalProgress.quizzesCompleted || 0; + const rDone = streakState.dailyGoalProgress.questionsReviewed || 0; + const quizGoalProgress = qDone / 1; + const reviewGoalProgress = rDone / 10; + dailyGoalPct = Math.min(100, Math.round(Math.max(quizGoalProgress, reviewGoalProgress) * 100)); + dailyGoalMetaText = dailyGoalPct >= 100 + ? `π Goal Achieved! (${qDone}/1 quiz, ${rDone}/10 reviewed)` + : `${qDone}/1 quiz, ${rDone}/10 reviewed today`; + } else { + const streak = window.quizProgress?.getStreak?.(); + currentStreak = streak?.currentStreak || 0; + const last = streak?.lastPracticeDate; + streakMetaText = last ? `Last practice: ${last}` : "No practice yet."; + } const streakValue = root.querySelector("#streakValue"); const streakMeta = root.querySelector("#streakMeta"); - if (streakValue) streakValue.textContent = String(streak?.currentStreak || 0); - if (streakMeta) { - const last = streak?.lastPracticeDate; - streakMeta.textContent = last ? `Last practice: ${last}` : "No practice yet."; - } + if (streakValue) streakValue.textContent = String(currentStreak); + if (streakMeta) streakMeta.textContent = streakMetaText; + + const dailyGoalValue = root.querySelector("#dailyGoalValue"); + const dailyGoalMeta = root.querySelector("#dailyGoalMeta"); + if (dailyGoalValue) dailyGoalValue.textContent = `${dailyGoalPct}%`; + if (dailyGoalMeta) dailyGoalMeta.textContent = dailyGoalMetaText; + const overall = window.quizProgress?.getOverallAccuracy?.(); const overallAccuracyValue = root.querySelector("#overallAccuracyValue"); const overallAccuracyMeta = root.querySelector("#overallAccuracyMeta"); @@ -214,6 +240,142 @@ }); } + function renderMastery(root) { + const masteryListEl = root.querySelector("#masterySkillsList"); + const weakSkillsEl = root.querySelector("#weakSkillsRecommendations"); + if (!masteryListEl || !weakSkillsEl) return; + + const mastery = window.quizProgress.getMasteryStats(); + const taxonomy = window.quizProgress.SKILL_TAXONOMY || {}; + + const skillMap = new Map(); + for (const [qText, tax] of Object.entries(taxonomy)) { + if (!skillMap.has(tax.skillId)) { + skillMap.set(tax.skillId, { + skillId: tax.skillId, + label: tax.label, + topicId: tax.topicId, + quizUrl: tax.quizUrl, + attempts: 0, + correct: 0 + }); + } + } + + let totalAttempts = 0; + for (const [sId, m] of Object.entries(mastery)) { + totalAttempts += m.attempts || 0; + if (skillMap.has(sId)) { + const entry = skillMap.get(sId); + entry.attempts = m.attempts || 0; + entry.correct = m.correct || 0; + } else { + skillMap.set(sId, { + skillId: sId, + label: sId.replace("-general", " General Concepts"), + topicId: sId.split("-")[0], + quizUrl: null, + attempts: m.attempts || 0, + correct: m.correct || 0 + }); + } + } + + const skillsArray = Array.from(skillMap.values()); + + skillsArray.sort((a, b) => { + if (a.attempts !== b.attempts) { + return b.attempts - a.attempts; + } + return a.label.localeCompare(b.label); + }); + + if (totalAttempts === 0) { + masteryListEl.innerHTML = `
Choose a subject to start learning:
+ + +Complete 1 quiz or review 10 questions to hit your goal today!
+Track your mastery levels per specific sub-skill/concept. Recommendations adapt to your weakest areas.
+ +Track mastery levels per specific sub-skill/concept. Recommendations adapt to the weakest areas.
+ +Choose your quiz mode below to begin.
+ ++ We recommend focusing on: ${nextRecText} +
+ ${nextRecUrl ? ` + + Practice Next Concept β + + ` : ""} + `; + } + + summaryContainer.innerHTML = improvedHtml + recHtml; + scoreEl.appendChild(summaryContainer); + } + // Initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); diff --git a/quizProgress.js b/quizProgress.js index 500f37e..7e21957 100644 --- a/quizProgress.js +++ b/quizProgress.js @@ -33,6 +33,97 @@ const QUIZ_TOPICS = [ { id: "chemistry-thermo", label: "Chemistry: Thermodynamics", subject: "chemistry", quizIds: ["quiz:thermo"] }, ]; +/** + * Central Skill Taxonomy mapping each question text to a unique skillId. + */ +const SKILL_TAXONOMY = { + // --- Physics: Motion --- + "What is the SI unit of speed?": { skillId: "motion-basics", label: "Speed & Velocity Basics", topicId: "physics-motion", quizUrl: "quiz/motionquiz.html" }, + "What causes an object to accelerate?": { skillId: "motion-forces", label: "Acceleration & Forces", topicId: "physics-motion", quizUrl: "quiz/motionquiz.html" }, + "Which of these is a scalar quantity?": { skillId: "motion-scalars", label: "Scalars & Vectors", topicId: "physics-motion", quizUrl: "quiz/motionquiz.html" }, + "What is the formula for acceleration?": { skillId: "motion-equations", label: "Equations of Motion", topicId: "physics-motion", quizUrl: "quiz/motionquiz.html" }, + + // --- Physics: Newton's Laws --- + "What does Newton's First Law state?": { skillId: "nlm-first-law", label: "Newton's First Law & Inertia", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "Which force is required to change the state of motion of an object?": { skillId: "nlm-forces", label: "Force & Equilibrium", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "Newton's Second Law states that force is equal to:": { skillId: "nlm-second-law", label: "Newton's Second Law (F=ma)", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "What happens when two equal and opposite forces act on an object?": { skillId: "nlm-forces", label: "Force & Equilibrium", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "According to Newtonβs Third Law, what happens when you push on a wall?": { skillId: "nlm-third-law", label: "Newton's Third Law (Action/Reaction)", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "What is inertia?": { skillId: "nlm-first-law", label: "Newton's First Law & Inertia", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "If an object's mass increases, what happens to the force required to accelerate it?": { skillId: "nlm-second-law", label: "Newton's Second Law (F=ma)", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + "Which of the following is an example of Newtonβs Third Law?": { skillId: "nlm-third-law", label: "Newton's Third Law (Action/Reaction)", topicId: "physics-nlm", quizUrl: "quiz/nlmquiz.html" }, + + // --- Physics: Projectile Motion --- + "What is the path of a projectile in ideal conditions?": { skillId: "projectile-trajectory", label: "Projectile Trajectory", topicId: "physics-projectile", quizUrl: "quiz/projectilequiz.html" }, + "What is the horizontal acceleration of a projectile in the absence of air resistance?": { skillId: "projectile-acceleration", label: "Horizontal & Vertical Acceleration", topicId: "physics-projectile", quizUrl: "quiz/projectilequiz.html" }, + "At the highest point of its trajectory, what is the vertical velocity of a projectile?": { skillId: "projectile-velocity", label: "Velocity at Peak", topicId: "physics-projectile", quizUrl: "quiz/projectilequiz.html" }, + "Which factor affects the range of a projectile the most?": { skillId: "projectile-range-height", label: "Range & Maximum Height", topicId: "physics-projectile", quizUrl: "quiz/projectilequiz.html" }, + "What is the optimal angle for maximum range in projectile motion (neglecting air resistance)?": { skillId: "projectile-range-height", label: "Range & Maximum Height", topicId: "physics-projectile", quizUrl: "quiz/projectilequiz.html" }, + + // --- Physics: Ray Optics --- + "What is the law of reflection?": { skillId: "ray-reflection", label: "Reflection Laws", topicId: "physics-ray", quizUrl: "quiz/rayquiz.html" }, + "Which mirror always forms a virtual, upright, and diminished image?": { skillId: "ray-mirrors", label: "Spherical Mirrors", topicId: "physics-ray", quizUrl: "quiz/rayquiz.html" }, + "What is the focal length of a plane mirror?": { skillId: "ray-mirrors", label: "Spherical Mirrors", topicId: "physics-ray", quizUrl: "quiz/rayquiz.html" }, + "What happens when light travels from a denser to a rarer medium at an angle greater than the critical angle?": { skillId: "ray-tir", label: "Total Internal Reflection", topicId: "physics-ray", quizUrl: "quiz/rayquiz.html" }, + "Which lens is used to correct myopia (nearsightedness)?": { skillId: "ray-lenses", label: "Lenses & Vision Correction", topicId: "physics-ray", quizUrl: "quiz/rayquiz.html" }, + + // --- Maths: Calculus --- + "What is the limit of sin(x)/x as x approaches 0?": { skillId: "calculus-limits", label: "Limits & Continuity", topicId: "maths-calculus", quizUrl: "mathsquiz/calculusquiz.html" }, + "What is the derivative of f(x) = 3xΒ² + 5x?": { skillId: "calculus-differentiation", label: "Differentiation Concepts", topicId: "maths-calculus", quizUrl: "mathsquiz/calculusquiz.html" }, + "What is the integral of g(x) = 4xΒ³ + 2xΒ² - 3x?": { skillId: "calculus-integration", label: "Integration & Antiderivatives", topicId: "maths-calculus", quizUrl: "mathsquiz/calculusquiz.html" }, + "What is the second derivative of f(x) = xΒ³ - 6xΒ² + 9x?": { skillId: "calculus-differentiation", label: "Differentiation Concepts", topicId: "maths-calculus", quizUrl: "mathsquiz/calculusquiz.html" }, + "Which of the following functions is NOT continuous at x = 2?": { skillId: "calculus-limits", label: "Limits & Continuity", topicId: "maths-calculus", quizUrl: "mathsquiz/calculusquiz.html" }, + + // --- Maths: Vectors --- + "What is the dot product of two perpendicular vectors?": { skillId: "vectors-dot-product", label: "Dot Product", topicId: "maths-vectors", quizUrl: "mathsquiz/vectorquiz.html" }, + "Which of the following is a scalar quantity?": { skillId: "vectors-scalars", label: "Scalars & Vectors", topicId: "maths-vectors", quizUrl: "mathsquiz/vectorquiz.html" }, + "If a vector has components (3, 4, 0), what is its magnitude?": { skillId: "vectors-magnitude", label: "Vector Magnitude", topicId: "maths-vectors", quizUrl: "mathsquiz/vectorquiz.html" }, + "The cross product of two parallel vectors is:": { skillId: "vectors-cross-product", label: "Cross Product", topicId: "maths-vectors", quizUrl: "mathsquiz/vectorquiz.html" }, + "In 3D space, how many components does a vector have?": { skillId: "vectors-components", label: "3D Vector Components", topicId: "maths-vectors", quizUrl: "mathsquiz/vectorquiz.html" }, + + // --- Maths: Probability --- + "If the mean of a dataset is 50 and the standard deviation is 5, what is the coefficient of variation?": { skillId: "probability-deviation", label: "Standard Deviation & Variation", topicId: "maths-probability", quizUrl: "mathsquiz/probabilityquiz.html" }, + "What is the probability of getting exactly one head when two fair coins are tossed?": { skillId: "probability-basic", label: "Basic Probability Concepts", topicId: "maths-probability", quizUrl: "mathsquiz/probabilityquiz.html" }, + "Which measure of central tendency is most affected by extreme values?": { skillId: "probability-central", label: "Central Tendency Measures", topicId: "maths-probability", quizUrl: "mathsquiz/probabilityquiz.html" }, + "A letter is chosen at random from the word 'STATISTICS'. What is the probability of selecting a vowel?": { skillId: "probability-basic", label: "Basic Probability Concepts", topicId: "maths-probability", quizUrl: "mathsquiz/probabilityquiz.html" }, + "In a normal distribution, approximately what percentage of data lies within one standard deviation from the mean?": { skillId: "probability-distribution", label: "Normal Distribution", topicId: "maths-probability", quizUrl: "mathsquiz/probabilityquiz.html" }, + + // --- Maths: Coordinate Geometry --- + "What is the distance between the points A(0, 6) and B(0, -2)?": { skillId: "geometry-distance", label: "Distance Formula", topicId: "maths-geometry", quizUrl: "mathsquiz/geometryquiz.html" }, + "The midpoint of the line segment joining (2, 3) and (4, 7) is:": { skillId: "geometry-midpoint", label: "Midpoint Formula", topicId: "maths-geometry", quizUrl: "mathsquiz/geometryquiz.html" }, + "If three points are collinear, then the area of the triangle formed by these points is:": { skillId: "geometry-collinearity", label: "Collinearity & Triangles", topicId: "maths-geometry", quizUrl: "mathsquiz/geometryquiz.html" }, + "Which quadrant does the point (-3, 5) lie in?": { skillId: "geometry-quadrants", label: "Quadrants & Coordinates", topicId: "maths-geometry", quizUrl: "mathsquiz/geometryquiz.html" }, + "The slope of a line perpendicular to a line with slope 2 is:": { skillId: "geometry-slope", label: "Slope & Line Properties", topicId: "maths-geometry", quizUrl: "mathsquiz/geometryquiz.html" }, + + // --- Chemistry: Atomic Structure --- + "Which of the following particles has a positive charge?": { skillId: "atomic-particles", label: "Subatomic Particles", topicId: "chemistry-atomic", quizUrl: "chemistryquiz/atomic_structurequiz.html" }, + "What is the trend in atomic radius as you move across a period from left to right?": { skillId: "atomic-periodic-trends", label: "Periodic Table Trends", topicId: "chemistry-atomic", quizUrl: "chemistryquiz/atomic_structurequiz.html" }, + "Which element has the highest electronegativity?": { skillId: "atomic-periodic-trends", label: "Periodic Table Trends", topicId: "chemistry-atomic", quizUrl: "chemistryquiz/atomic_structurequiz.html" }, + "What is the electron configuration of a neutral oxygen atom?": { skillId: "atomic-electron-config", label: "Electron Configurations", topicId: "chemistry-atomic", quizUrl: "chemistryquiz/atomic_structurequiz.html" }, + "Which of the following statements is true about elements in the same group of the periodic table?": { skillId: "atomic-periodic-trends", label: "Periodic Table Trends", topicId: "chemistry-atomic", quizUrl: "chemistryquiz/atomic_structurequiz.html" }, + + // --- Chemistry: Chemical Bonding --- + "What is the molecular geometry of COβ?": { skillId: "bonding-geometry", label: "Molecular Geometry", topicId: "chemistry-bonding", quizUrl: "chemistryquiz/chemical_bondingquiz.html" }, + "Which of the following molecules has a trigonal pyramidal shape?": { skillId: "bonding-geometry", label: "Molecular Geometry", topicId: "chemistry-bonding", quizUrl: "chemistryquiz/chemical_bondingquiz.html" }, + "What is the hybridization of the central atom in BFβ?": { skillId: "bonding-hybridization", label: "Atom Hybridization", topicId: "chemistry-bonding", quizUrl: "chemistryquiz/chemical_bondingquiz.html" }, + "Which molecule has a bent molecular geometry due to lone pair repulsion?": { skillId: "bonding-geometry", label: "Molecular Geometry", topicId: "chemistry-bonding", quizUrl: "chemistryquiz/chemical_bondingquiz.html" }, + "What is the bond angle in a tetrahedral molecule like CHβ?": { skillId: "bonding-angles", label: "Bond Angles & Shapes", topicId: "chemistry-bonding", quizUrl: "chemistryquiz/chemical_bondingquiz.html" }, + + // --- Chemistry: Equilibrium --- + "What happens to the equilibrium position when the concentration of a reactant is increased?": { skillId: "equil-le-chatelier", label: "Le Chatelier's Principle", topicId: "chemistry-equil", quizUrl: "chemistryquiz/equilibriumquiz.html" }, + "In an exothermic reaction at equilibrium, what is the effect of increasing the temperature?": { skillId: "equil-le-chatelier", label: "Le Chatelier's Principle", topicId: "chemistry-equil", quizUrl: "chemistryquiz/equilibriumquiz.html" }, + "Adding an inert gas at constant volume to a gaseous equilibrium system will:": { skillId: "equil-le-chatelier", label: "Le Chatelier's Principle", topicId: "chemistry-equil", quizUrl: "chemistryquiz/equilibriumquiz.html" }, + "Which of the following factors does NOT affect the position of equilibrium?": { skillId: "equil-factors", label: "Equilibrium Factors", topicId: "chemistry-equil", quizUrl: "chemistryquiz/equilibriumquiz.html" }, + "For the reaction: Nβ(g) + 3Hβ(g) β 2NHβ(g), decreasing the pressure will:": { skillId: "equil-le-chatelier", label: "Le Chatelier's Principle", topicId: "chemistry-equil", quizUrl: "chemistryquiz/equilibriumquiz.html" }, + + // --- Chemistry: Thermodynamics (and Kinetics) --- + "Which of the following is NOT a state function in thermodynamics?": { skillId: "thermo-state-functions", label: "State Functions", topicId: "chemistry-thermo", quizUrl: "chemistryquiz/thermoquiz.html" }, + "According to the second law of thermodynamics, which statement is true?": { skillId: "thermo-second-law", label: "Second Law & Entropy", topicId: "chemistry-thermo", quizUrl: "chemistryquiz/thermoquiz.html" }, + "In the Arrhenius equation, what does the activation energy (Ea) represent?": { skillId: "thermo-activation-energy", label: "Activation Energy", topicId: "chemistry-thermo", quizUrl: "chemistryquiz/thermoquiz.html" }, + "How does a catalyst affect the rate of a chemical reaction?": { skillId: "thermo-catalysts", label: "Catalysts & Reaction Rates", topicId: "chemistry-thermo", quizUrl: "chemistryquiz/thermoquiz.html" }, + "Which factor does NOT affect the rate of a chemical reaction?": { skillId: "thermo-reaction-rates", label: "Reaction Rates Factors", topicId: "chemistry-thermo", quizUrl: "chemistryquiz/thermoquiz.html" } +}; + function _todayLocalISODate() { // YYYY-MM-DD in local time const d = new Date(); @@ -53,17 +144,18 @@ function _parseISODateToUTCStart(isoDateYYYYMMDD) { function _loadState() { try { const raw = localStorage.getItem(QUIZ_PROGRESS_KEY); - if (!raw) return { attempts: [], byTopic: {}, streak: { lastPracticeDate: null, currentStreak: 0 } }; + if (!raw) return { attempts: [], byTopic: {}, streak: { lastPracticeDate: null, currentStreak: 0 }, mastery: {} }; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") { - return { attempts: [], byTopic: {}, streak: { lastPracticeDate: null, currentStreak: 0 } }; + return { attempts: [], byTopic: {}, streak: { lastPracticeDate: null, currentStreak: 0 }, mastery: {} }; } if (!Array.isArray(parsed.attempts)) parsed.attempts = []; if (!parsed.byTopic || typeof parsed.byTopic !== "object") parsed.byTopic = {}; if (!parsed.streak || typeof parsed.streak !== "object") parsed.streak = { lastPracticeDate: null, currentStreak: 0 }; + if (!parsed.mastery || typeof parsed.mastery !== "object") parsed.mastery = {}; return parsed; } catch { - return { attempts: [], byTopic: {}, streak: { lastPracticeDate: null, currentStreak: 0 } }; + return { attempts: [], byTopic: {}, streak: { lastPracticeDate: null, currentStreak: 0 }, mastery: {} }; } } @@ -91,6 +183,83 @@ function recordAttempt({ topicId, score, totalQuestions, correctCount, timeTaken const now = Date.now(); const today = _todayLocalISODate(); + // --- Skill Mastery Tracking --- + // Attempt to resolve individual questions/answers from global scope + let resolvedQuestions = []; + let resolvedAnswers = []; + + if (window.adaptiveSteps && window.adaptiveSteps.length > 0) { + resolvedQuestions = window.adaptiveSteps; + resolvedAnswers = window.userSelectionsByStep || []; + } else if (window.questions && window.questions.length > 0) { + resolvedQuestions = window.questions; + resolvedAnswers = window.userAnswers || []; + } + + let calculatedCorrect = 0; + let hasAnswers = false; + + if (resolvedQuestions.length > 0) { + resolvedQuestions.forEach((q, idx) => { + const ans = resolvedAnswers[idx]; + if (ans !== undefined && ans !== null) { + hasAnswers = true; + // Determine correct option (which might be index number or string) + const correctOption = typeof q.answer === 'number' && Array.isArray(q.options) ? q.options[q.answer] : q.answer; + const isCorrect = ans === correctOption; + + if (isCorrect) { + calculatedCorrect++; + } + + // Find skill from taxonomy mapping + const qText = q.question ? q.question.trim() : ""; + const taxonomyMatch = SKILL_TAXONOMY[qText]; + if (taxonomyMatch) { + const sId = taxonomyMatch.skillId; + if (!state.mastery[sId]) { + state.mastery[sId] = { attempts: 0, correct: 0, lastAttemptAt: 0, weaknessAttempts: 0, weaknessCorrect: 0 }; + } + state.mastery[sId].attempts += 1; + if (isCorrect) { + state.mastery[sId].correct += 1; + } + state.mastery[sId].lastAttemptAt = now; + if (window.isWeaknessFocusMode) { + state.mastery[sId].weaknessAttempts = (state.mastery[sId].weaknessAttempts || 0) + 1; + if (isCorrect) { + state.mastery[sId].weaknessCorrect = (state.mastery[sId].weaknessCorrect || 0) + 1; + } + } + } else { + // Fallback to topic-general skill if not mapped explicitly + const fallbackSkillId = topicId + "-general"; + if (!state.mastery[fallbackSkillId]) { + state.mastery[fallbackSkillId] = { attempts: 0, correct: 0, lastAttemptAt: 0, weaknessAttempts: 0, weaknessCorrect: 0 }; + } + state.mastery[fallbackSkillId].attempts += 1; + if (isCorrect) { + state.mastery[fallbackSkillId].correct += 1; + } + state.mastery[fallbackSkillId].lastAttemptAt = now; + if (window.isWeaknessFocusMode) { + state.mastery[fallbackSkillId].weaknessAttempts = (state.mastery[fallbackSkillId].weaknessAttempts || 0) + 1; + if (isCorrect) { + state.mastery[fallbackSkillId].weaknessCorrect = (state.mastery[fallbackSkillId].weaknessCorrect || 0) + 1; + } + } + } + } + }); + } + + // If we successfully calculated question-level details, override aggregate parameters + if (hasAnswers) { + correctCount = calculatedCorrect; + score = calculatedCorrect; + totalQuestions = resolvedQuestions.length; + } + const total = Number(totalQuestions) || 0; const got = Number(score) || 0; @@ -159,11 +328,11 @@ function recordAttempt({ topicId, score, totalQuestions, correctCount, timeTaken totalQuestions: total, correctCount: correct, accuracy: total > 0 && typeof correct === "number" && Number.isFinite(correct) ? correct / total : null, - timeTakenMs: timeMs, startedAt: null, finishedAt: now, practiceDate: today, + isWeaknessFocus: window.isWeaknessFocusMode || false, }); // Keep attempts bounded @@ -195,9 +364,13 @@ function recordAttempt({ topicId, score, totalQuestions, correctCount, timeTaken state.streak = s; _saveState(state); + // Update unified streak & daily goal state + _updateUnifiedStreakAndGoal("quiz", 1); + return state; } + function getStreak() { const state = _loadState(); const s = state.streak || { lastPracticeDate: null, currentStreak: 0 }; @@ -359,8 +532,137 @@ function getRecommendedTopics({ limit = 3 } = {}) { return topicScores.slice(0, limit); } +function getMasteryStats() { + const state = _loadState(); + return state.mastery || {}; +} + +function getWeakestSkills({ limit = 3 } = {}) { + const state = _loadState(); + const mastery = state.mastery || {}; + + const skillIds = new Set(); + const skillsList = []; + + for (const [qText, tax] of Object.entries(SKILL_TAXONOMY)) { + if (!skillIds.has(tax.skillId)) { + skillIds.add(tax.skillId); + + const m = mastery[tax.skillId] || { attempts: 0, correct: 0, lastAttemptAt: null }; + const accuracy = m.attempts > 0 ? m.correct / m.attempts : null; + + // Weakness score: + // - Attempted skills with low accuracy have highest weakness (e.g. 1 - accuracy) + // - Unattempted skills have weakness of 0.8 + let weakness = 0; + if (m.attempts === 0) { + weakness = 0.8; + } else { + weakness = 1 - accuracy; + } + + skillsList.push({ + skillId: tax.skillId, + label: tax.label, + topicId: tax.topicId, + quizUrl: tax.quizUrl, + attempts: m.attempts, + correct: m.correct, + accuracy: accuracy, + weakness: weakness + }); + } + } + + skillsList.sort((a, b) => b.weakness - a.weakness); + return skillsList.slice(0, limit); +} + +function getQuestionWeaknessWeight(q) { + if (!q || !q.question) return 0.5; + const qText = q.question.trim(); + const tax = SKILL_TAXONOMY[qText]; + if (!tax) return 0.5; + + const stats = getMasteryStats(); + const m = stats[tax.skillId]; + if (!m || m.attempts === 0) return 0.5; + + const accuracy = m.correct / m.attempts; + return 1.0 - accuracy; +} + +function _updateUnifiedStreakAndGoal(type, value = 1) { + if (window.studyProgress && typeof window.studyProgress.recordActivity === "function") { + window.studyProgress.recordActivity(type, value); + return; + } + + const STREAK_KEY = "learnsphere_streak_state_v1"; + const today = _todayLocalISODate(); + let state = { lastActiveDate: null, currentStreak: 0, dailyGoalProgress: { quizzesCompleted: 0, questionsReviewed: 0 } }; + + try { + const raw = localStorage.getItem(STREAK_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + state = parsed; + } + } + } catch (e) {} + + if (!state.dailyGoalProgress || typeof state.dailyGoalProgress !== "object") { + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + + const lastActive = state.lastActiveDate; + const todayToken = _parseISODateToUTCStart(today); + const lastToken = lastActive ? _parseISODateToUTCStart(lastActive) : null; + + if (lastActive) { + if (todayToken > lastToken + 1) { + state.currentStreak = 0; + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } else if (lastActive !== today) { + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + } else { + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + + if (!lastActive) { + state.currentStreak = 1; + state.lastActiveDate = today; + } else if (lastActive === today) { + // Same day + } else if (lastToken !== null && todayToken === lastToken + 1) { + state.currentStreak += 1; + state.lastActiveDate = today; + } else { + state.currentStreak = 1; + state.lastActiveDate = today; + } + + if (type === "quiz") { + state.dailyGoalProgress.quizzesCompleted += value; + } else if (type === "review") { + state.dailyGoalProgress.questionsReviewed += value; + } + + try { + localStorage.setItem(STREAK_KEY, JSON.stringify(state)); + } catch (e) {} + + // Trigger achievements check if achievements is loaded + if (window.achievements && typeof window.achievements.checkAndNotify === "function") { + window.achievements.checkAndNotify(); + } +} + window.quizProgress = { QUIZ_TOPICS, + SKILL_TAXONOMY, recordAttempt, getStreak, getTopicStats, @@ -368,5 +670,9 @@ window.quizProgress = { getAccuracySeries, getOverallAccuracy, getRecommendedTopics, + getMasteryStats, + getWeakestSkills, + getQuestionWeaknessWeight, }; + diff --git a/review.js b/review.js index ac30a62..deff799 100644 --- a/review.js +++ b/review.js @@ -96,7 +96,88 @@ } } + function _updateUnifiedStreakAndGoal(type, value = 1) { + if (window.studyProgress && typeof window.studyProgress.recordActivity === "function") { + window.studyProgress.recordActivity(type, value); + return; + } + + const STREAK_KEY = "learnsphere_streak_state_v1"; + const today = (function() { + 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 parseISODateToUTCStart(isoDateYYYYMMDD) { + const [y, m, d] = isoDateYYYYMMDD.split("-").map(Number); + const dt = new Date(y, m - 1, d, 0, 0, 0, 0); + return Math.floor(dt.getTime() / 86400000); + } + + let state = { lastActiveDate: null, currentStreak: 0, dailyGoalProgress: { quizzesCompleted: 0, questionsReviewed: 0 } }; + + try { + const raw = localStorage.getItem(STREAK_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + state = parsed; + } + } + } catch (e) {} + + if (!state.dailyGoalProgress || typeof state.dailyGoalProgress !== "object") { + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + + const lastActive = state.lastActiveDate; + const todayToken = parseISODateToUTCStart(today); + const lastToken = lastActive ? parseISODateToUTCStart(lastActive) : null; + + if (lastActive) { + if (todayToken > lastToken + 1) { + state.currentStreak = 0; + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } else if (lastActive !== today) { + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + } else { + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + + if (!lastActive) { + state.currentStreak = 1; + state.lastActiveDate = today; + } else if (lastActive === today) { + // Same day + } else if (lastToken !== null && todayToken === lastToken + 1) { + state.currentStreak += 1; + state.lastActiveDate = today; + } else { + state.currentStreak = 1; + state.lastActiveDate = today; + } + + if (type === "quiz") { + state.dailyGoalProgress.quizzesCompleted += value; + } else if (type === "review") { + state.dailyGoalProgress.questionsReviewed += value; + } + + try { + localStorage.setItem(STREAK_KEY, JSON.stringify(state)); + } catch (e) {} + + if (window.achievements && typeof window.achievements.checkAndNotify === "function") { + window.achievements.checkAndNotify(); + } + } + function recordReviewResult({ topicId, scorePct, answeredCount = 0 }) { + if (!topicId) return; const today = _todayLocalISODate(); @@ -145,9 +226,13 @@ } saveHistory(history); + // Update unified daily streak & daily goal state + _updateUnifiedStreakAndGoal("review", answeredCount); + return schedule[topicId]; } + function skipTopic(topicId) { if (!topicId) return; const today = _todayLocalISODate(); diff --git a/teachers.html b/teachers.html index f664eee..ff576a9 100644 --- a/teachers.html +++ b/teachers.html @@ -93,8 +93,14 @@Track mastery levels per specific sub-skill/concept. Recommendations adapt to the weakest areas.
+ +