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 = `
No mastery data recorded yet.
`; + } else { + masteryListEl.innerHTML = ""; + skillsArray.forEach(s => { + if (s.attempts === 0) return; + + const accuracy = s.attempts > 0 ? s.correct / s.attempts : 0; + const pctValue = Math.round(accuracy * 100); + + let barColor = "#ef4444"; + if (pctValue >= 80) { + barColor = "#66fcf1"; + } else if (pctValue >= 50) { + barColor = "#f59e0b"; + } + + const row = document.createElement("div"); + row.className = "topic-row"; + row.style.marginBottom = "8px"; + row.style.display = "flex"; + row.style.alignItems = "center"; + row.style.justifyContent = "space-between"; + row.style.padding = "10px 12px"; + row.style.borderRadius = "10px"; + row.style.background = "rgba(255,255,255,0.03)"; + row.style.border = "1px solid rgba(255,255,255,0.06)"; + row.innerHTML = ` +
${s.label}
+
+ +
+
+
${pctValue}%
+
${s.correct}/${s.attempts} correct
+
+ `; + masteryListEl.appendChild(row); + }); + + if (masteryListEl.children.length === 0) { + masteryListEl.innerHTML = `
No concept stats available yet.
`; + } + } + + const weakSkills = window.quizProgress.getWeakestSkills({ limit: 3 }); + + weakSkillsEl.innerHTML = ""; + if (!weakSkills || weakSkills.length === 0) { + weakSkillsEl.innerHTML = `
No weak concepts identified yet.
`; + } else { + weakSkills.forEach(ws => { + const accuracyPct = ws.attempts > 0 ? `${Math.round(ws.accuracy * 100)}%` : "Not attempted"; + + const card = document.createElement("div"); + card.style.background = "rgba(255, 255, 255, 0.02)"; + card.style.border = "1px solid rgba(255, 255, 255, 0.06)"; + card.style.borderRadius = "8px"; + card.style.padding = "12px 14px"; + card.style.display = "flex"; + card.style.justifyContent = "space-between"; + card.style.alignItems = "center"; + + let statusColor = "#ff5e5e"; + if (ws.attempts === 0) { + statusColor = "#a0aec0"; + } else if (ws.accuracy >= 0.8) { + statusColor = "#66fcf1"; + } else if (ws.accuracy >= 0.5) { + statusColor = "#f59e0b"; + } + + card.innerHTML = ` +
+
${ws.label}
+
+ Accuracy: ${accuracyPct} + ${ws.attempts > 0 ? `(${ws.attempts} attempts)` : ""} +
+
+ `; + weakSkillsEl.appendChild(card); + }); + } + } + function renderAll(root) { // Guard: quizProgress must be loaded if (!window.quizProgress) { @@ -226,6 +388,7 @@ renderAccuracyChart(root); renderRecommendations(root); renderTopicStats(root); + renderMastery(root); } function initByRole() { diff --git a/home.html b/home.html index 932a84c..9904b94 100644 --- a/home.html +++ b/home.html @@ -235,9 +235,15 @@
-

Welcome to LearnSphere

+
+

Welcome to LearnSphere

+
+ πŸ”₯ 0 Day Streak +
+

Choose a subject to start learning:

+ + +
+
+ + 🎯 Daily Learning Goal + + 0% +
+
+
+
+

Complete 1 quiz or review 10 questions to hit your goal today!

+
+ +
Unlock milestones based on quiz attempts, practice streak, and accuracy. diff --git a/home.js b/home.js index 15a32c4..e43b1a0 100644 --- a/home.js +++ b/home.js @@ -31,10 +31,14 @@ document.addEventListener("DOMContentLoaded", () => { window.achievements.renderBadges("badgesContainerHome"); } - // Render Daily Streak stats - if (window.quizProgress && typeof window.quizProgress.getStreak === "function") { - const streak = window.quizProgress.getStreak(); - const days = streak.currentStreak || 0; + // Render Daily Streak & Daily Goal stats + if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") { + const streakState = window.studyProgress.loadStreakState(); + const days = streakState.currentStreak || 0; + + // Update streak elements + const headerCountEl = document.getElementById("headerStreakCount"); + if (headerCountEl) headerCountEl.textContent = String(days); const daysEl = document.getElementById("streakDaysText"); const barFillEl = document.getElementById("streakProgressBarFill"); @@ -51,16 +55,13 @@ document.addEventListener("DOMContentLoaded", () => { const pct = Math.min(100, Math.round((days / nextMilestone) * 100)); if (barFillEl) barFillEl.style.width = `${pct}%`; - // Streak dates are stored in quizProgress.js as local YYYY-MM-DD. - // Avoid toLocaleDateString formatting mismatches across browsers/timezones. 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"); const todayStr = `${yyyy}-${mm}-${dd}`; - const practicedToday = streak.lastPracticeDate === todayStr; + const practicedToday = streakState.lastActiveDate === todayStr; - if (descEl) { if (practicedToday) { descEl.innerHTML = `πŸ”₯ Streak safe for today! Practice tomorrow to build toward your ${nextMilestone}-day milestone.`; @@ -70,6 +71,29 @@ document.addEventListener("DOMContentLoaded", () => { descEl.innerHTML = `🏁 Start a quiz today to begin your daily practice streak! Next milestone: 3 days.`; } } + + // Render Daily Goal card + const dailyGoalPercentEl = document.getElementById("dailyGoalPercentText"); + const dailyGoalBarFillEl = document.getElementById("dailyGoalProgressBarFill"); + const dailyGoalStatusEl = document.getElementById("dailyGoalStatusText"); + + const qDone = streakState.dailyGoalProgress.quizzesCompleted || 0; + const rDone = streakState.dailyGoalProgress.questionsReviewed || 0; + + const quizGoalProgress = qDone / 1; + const reviewGoalProgress = rDone / 10; + const goalPercent = Math.min(100, Math.round(Math.max(quizGoalProgress, reviewGoalProgress) * 100)); + + if (dailyGoalPercentEl) dailyGoalPercentEl.textContent = `${goalPercent}%`; + if (dailyGoalBarFillEl) dailyGoalBarFillEl.style.width = `${goalPercent}%`; + + if (dailyGoalStatusEl) { + if (goalPercent >= 100) { + dailyGoalStatusEl.innerHTML = `πŸŽ‰ Daily Goal Achieved! Great job keeping up your learning momentum today! (${qDone}/1 quiz, ${rDone}/10 questions reviewed)`; + } else { + dailyGoalStatusEl.textContent = `Completed ${qDone}/1 quiz or reviewed ${rDone}/10 questions today. Keep going!`; + } + } } // Check for explain_mistake redirect diff --git a/my_progress.html b/my_progress.html index a94ffdf..5faa1f9 100644 --- a/my_progress.html +++ b/my_progress.html @@ -154,6 +154,11 @@

My Progress

β€”
β€”
+
+
Daily Goal
+
β€”
+
β€”
+
@@ -170,6 +175,29 @@

Recommended next

Recommendations focus on weaker or less-attempted topics. +
+
+ + +
+

+ 🎯 Personalized Mastery Dashboard +

+

Track your mastery levels per specific sub-skill/concept. Recommendations adapt to your weakest areas.

+ +
+
+

Concept Mastery Details

+
+
No mastery data recorded yet. Complete quizzes to see concept-wise stats!
+
+
+ +
+

Top Weak Concepts & Practice

+
+
No weak concepts identified yet.
+
@@ -199,6 +227,7 @@

πŸ—“οΈ Review queue

+ diff --git a/my_progress.js b/my_progress.js index e519aaf..e6ff4aa 100644 --- a/my_progress.js +++ b/my_progress.js @@ -91,15 +91,41 @@ function drawLineChart(canvas, labels, accuracyByDay) { } function init() { - const streak = window.quizProgress.getStreak(); + 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 = document.getElementById("streakValue"); const streakMeta = document.getElementById("streakMeta"); + if (streakValue) streakValue.textContent = String(currentStreak); + if (streakMeta) streakMeta.textContent = streakMetaText; - if (streakValue) streakValue.textContent = String(streak.currentStreak || 0); - if (streakMeta) { - const last = streak.lastPracticeDate; - streakMeta.textContent = last ? `Last practice: ${last}` : "No practice yet."; - } + const dailyGoalValue = document.getElementById("dailyGoalValue"); + const dailyGoalMeta = document.getElementById("dailyGoalMeta"); + if (dailyGoalValue) dailyGoalValue.textContent = `${dailyGoalPct}%`; + if (dailyGoalMeta) dailyGoalMeta.textContent = dailyGoalMetaText; const overall = window.quizProgress.getOverallAccuracy(); const overallAccuracyValue = document.getElementById("overallAccuracyValue"); @@ -243,8 +269,173 @@ function init() { } } +function initMasteryDashboard() { + const masteryListEl = document.getElementById("masterySkillsList"); + const weakSkillsEl = document.getElementById("weakSkillsRecommendations"); + if (!masteryListEl || !weakSkillsEl) return; + + const mastery = window.quizProgress.getMasteryStats(); + const taxonomy = window.quizProgress.SKILL_TAXONOMY || {}; + + // Group all unique skills from 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 + }); + } + } + + // Populate actual attempts and correct values + 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 { + // Fallback for dynamically generated or legacy general skills + 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()); + + // Render Left Column: Concept Mastery Details + // Sort attempted first, then alphabetical by label + skillsArray.sort((a, b) => { + if (a.attempts !== b.attempts) { + return b.attempts - a.attempts; // attempted first + } + return a.label.localeCompare(b.label); + }); + + if (totalAttempts === 0) { + masteryListEl.innerHTML = `
No mastery data recorded yet. Complete quizzes to see concept-wise stats!
`; + } else { + masteryListEl.innerHTML = ""; + skillsArray.forEach(s => { + // Only render if attempted to keep the dashboard clean and focused on progress + if (s.attempts === 0) return; + + const accuracy = s.attempts > 0 ? s.correct / s.attempts : 0; + const pctValue = Math.round(accuracy * 100); + + // Determine premium color based on accuracy + let barColor = "#ef4444"; // red for <50% + if (pctValue >= 80) { + barColor = "#66fcf1"; // teal for >=80% + } else if (pctValue >= 50) { + barColor = "#f59e0b"; // orange/yellow for 50-80% + } + + const row = document.createElement("div"); + row.className = "topic-row"; + row.style.marginBottom = "8px"; + row.innerHTML = ` +
${s.label}
+
+ +
+
+
${pctValue}%
+
${s.correct}/${s.attempts} correct
+
+ `; + masteryListEl.appendChild(row); + }); + + if (masteryListEl.children.length === 0) { + masteryListEl.innerHTML = `
Complete quizzes to see concept-wise stats!
`; + } + } + + // Render Right Column: Weakest Concepts & Recommended Practice + const weakSkills = window.quizProgress.getWeakestSkills({ limit: 3 }); + + weakSkillsEl.innerHTML = ""; + if (!weakSkills || weakSkills.length === 0) { + weakSkillsEl.innerHTML = `
No weak concepts identified yet. Keep practicing!
`; + } else { + weakSkills.forEach(ws => { + const accuracyPct = ws.attempts > 0 ? `${Math.round(ws.accuracy * 100)}%` : "Not attempted"; + + const card = document.createElement("div"); + card.style.background = "rgba(255, 255, 255, 0.02)"; + card.style.border = "1px solid rgba(255, 255, 255, 0.06)"; + card.style.borderRadius = "8px"; + card.style.padding = "12px 14px"; + card.style.display = "flex"; + card.style.justifyContent = "space-between"; + card.style.alignItems = "center"; + card.style.transition = "all 0.2s ease"; + + card.addEventListener("mouseenter", () => { + card.style.background = "rgba(255, 255, 255, 0.04)"; + card.style.borderColor = "rgba(102, 252, 241, 0.3)"; + }); + card.addEventListener("mouseleave", () => { + card.style.background = "rgba(255, 255, 255, 0.02)"; + card.style.borderColor = "rgba(255, 255, 255, 0.06)"; + }); + + const practiceUrl = ws.quizUrl ? ws.quizUrl : "#"; + + let statusColor = "#ff5e5e"; + if (ws.attempts === 0) { + statusColor = "#a0aec0"; + } else if (ws.accuracy >= 0.8) { + statusColor = "#66fcf1"; + } else if (ws.accuracy >= 0.5) { + statusColor = "#f59e0b"; + } + + card.innerHTML = ` +
+
${ws.label}
+
+ Accuracy: ${accuracyPct} + ${ws.attempts > 0 ? `(${ws.attempts} attempts)` : ""} +
+
+ ${ws.quizUrl ? ` + + Practice βž” + + ` : ""} + `; + weakSkillsEl.appendChild(card); + }); + } +} + document.addEventListener("DOMContentLoaded", () => { init(); + initMasteryDashboard(); initReviewQueueWidget(); if (window.achievements?.renderBadges) { window.achievements.renderBadges("badgesContainerMyProgress"); diff --git a/parents.html b/parents.html index a8fc05a..af40d12 100644 --- a/parents.html +++ b/parents.html @@ -72,8 +72,14 @@

Parent Progress Dashboard

β€”
β€”
+
+
Daily Goal
+
β€”
+
β€”
+
+

Accuracy over time

@@ -88,6 +94,30 @@

Weak topics (recommended)

+ +
+

+ 🎯 Personalized Concept Mastery +

+

Track mastery levels per specific sub-skill/concept. Recommendations adapt to the weakest areas.

+ +
+
+

Concept Mastery Details

+
+
No mastery data recorded yet.
+
+
+ +
+

Top Weak Concepts

+
+
No weak concepts identified yet.
+
+
+
+
+

Topic-wise performance

@@ -98,6 +128,7 @@

Topic-wise performance

+ diff --git a/progress.js b/progress.js index 14ee3b1..53fd6a1 100644 --- a/progress.js +++ b/progress.js @@ -168,9 +168,15 @@ function recordReviewResult({ topicId, scorePct, answeredCount = 0 }) { }; saveReviewSchedule(scheduleMap); + + if (window.studyProgress && typeof window.studyProgress.recordActivity === "function") { + window.studyProgress.recordActivity("review", answeredCount); + } + return scheduleMap[topicId]; } + function formatDaysUntil(isoDate) { if (!isoDate) return ""; const todayToken = _parseISODateToUTCStart(_todayLocalISODate()); @@ -271,8 +277,93 @@ function updateProgressSummary() { summaryEl.textContent = `${completed} of ${TOPICS.length} topics completed (${pct}%)`; } +// ── Unified Streaks & Daily Goals API ───────────────────────────────────────── + +window.studyProgress = { + STREAK_KEY: "learnsphere_streak_state_v1", + + loadStreakState() { + try { + const raw = localStorage.getItem(this.STREAK_KEY); + if (!raw) return { lastActiveDate: null, currentStreak: 0, dailyGoalProgress: { quizzesCompleted: 0, questionsReviewed: 0 } }; + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== "object") { + return { lastActiveDate: null, currentStreak: 0, dailyGoalProgress: { quizzesCompleted: 0, questionsReviewed: 0 } }; + } + if (!parsed.dailyGoalProgress || typeof parsed.dailyGoalProgress !== "object") { + parsed.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + + const today = _todayLocalISODate(); + if (parsed.lastActiveDate) { + const todayToken = _parseISODateToUTCStart(today); + const lastToken = _parseISODateToUTCStart(parsed.lastActiveDate); + if (todayToken > lastToken + 1) { + parsed.currentStreak = 0; + } + if (parsed.lastActiveDate !== today) { + parsed.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + } else { + parsed.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + return parsed; + } catch { + return { lastActiveDate: null, currentStreak: 0, dailyGoalProgress: { quizzesCompleted: 0, questionsReviewed: 0 } }; + } + }, + + saveStreakState(state) { + try { + localStorage.setItem(this.STREAK_KEY, JSON.stringify(state)); + } catch (e) { + console.warn("LearnSphere: Could not save streak state.", e); + } + }, + + recordActivity(type, value = 1) { + const today = _todayLocalISODate(); + const state = this.loadStreakState(); + + const lastActive = state.lastActiveDate; + const todayToken = _parseISODateToUTCStart(today); + const lastToken = lastActive ? _parseISODateToUTCStart(lastActive) : null; + + if (!lastActive) { + state.currentStreak = 1; + state.lastActiveDate = today; + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } else if (lastActive === today) { + // Already active today, no change to streak date or count + } else if (lastToken !== null && todayToken === lastToken + 1) { + state.currentStreak += 1; + state.lastActiveDate = today; + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } else { + state.currentStreak = 1; + state.lastActiveDate = today; + state.dailyGoalProgress = { quizzesCompleted: 0, questionsReviewed: 0 }; + } + + if (type === "quiz") { + state.dailyGoalProgress.quizzesCompleted += value; + } else if (type === "review") { + state.dailyGoalProgress.questionsReviewed += value; + } + + this.saveStreakState(state); + + if (window.achievements && typeof window.achievements.checkAndNotify === "function") { + window.achievements.checkAndNotify(); + } + + return state; + } +}; + // ── Init ────────────────────────────────────────────────────────────────────── + document.addEventListener("DOMContentLoaded", () => { renderProgressList(); updateProgressSummary(); diff --git a/quiz/adaptiveQuiz.js b/quiz/adaptiveQuiz.js index 3d8bed8..df833fd 100644 --- a/quiz/adaptiveQuiz.js +++ b/quiz/adaptiveQuiz.js @@ -38,12 +38,21 @@ buckets[di].push({ ...q, __qid: idx }); }); - // Shuffle each bucket for variety. + // Shuffle or sort each bucket based on mode. 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]]; + if (window.isWeaknessFocusMode && window.quizProgress && typeof window.quizProgress.getQuestionWeaknessWeight === 'function') { + arr.sort((a, b) => { + const weightA = window.quizProgress.getQuestionWeaknessWeight(a); + const weightB = window.quizProgress.getQuestionWeaknessWeight(b); + return weightB - weightA; // weakest first + }); + } else { + // Shuffle each bucket for variety. + 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]]; + } } }); diff --git a/quizAssignmentHelper.js b/quizAssignmentHelper.js index 6626472..47f6d76 100644 --- a/quizAssignmentHelper.js +++ b/quizAssignmentHelper.js @@ -156,6 +156,7 @@ if (finalScore !== null && !isNaN(finalScore)) { handleQuizCompletion(finalScore, totalQs); injectExplainMistakeButtons(); + injectSessionSummary(); } else { console.warn("LearnSphere: Could not determine final score for submission / tracking."); } @@ -259,6 +260,21 @@ } function init() { + // Normalize questions (convert integer index answers to string options) + if (window.questions && Array.isArray(window.questions)) { + window.questions.forEach(q => { + if (typeof q.answer === 'number' && Array.isArray(q.options)) { + q.answer = q.options[q.answer]; + } + }); + } + + const topicId = getTopicIdFromPath(); + if (topicId) { + injectStartScreenStyles(); + showStartScreen(); + } + const originalShowResults = window.showResults; if (typeof originalShowResults === 'function') { window.showResults = wrapShowResults(originalShowResults); @@ -269,6 +285,16 @@ checkCount++; if (typeof window.showResults === 'function' && window.showResults !== window._wrappedShowResults) { clearInterval(checkInterval); + + // Normalize dynamically loaded questions as well + if (window.questions && Array.isArray(window.questions)) { + window.questions.forEach(q => { + if (typeof q.answer === 'number' && Array.isArray(q.options)) { + q.answer = q.options[q.answer]; + } + }); + } + window.showResults = wrapShowResults(window.showResults); window._wrappedShowResults = window.showResults; console.log("LearnSphere: Hooked into dynamically loaded showResults function."); @@ -278,6 +304,254 @@ } } + function injectStartScreenStyles() { + const style = document.createElement("style"); + style.textContent = ` + .quiz-start-card { + background: #0f1115; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 24px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + text-align: center; + max-width: 500px; + margin: 40px auto; + } + .mode-option-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + cursor: pointer; + text-align: left; + display: flex; + align-items: center; + gap: 12px; + transition: all 0.2s ease; + } + .mode-option-card:hover { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(102, 252, 241, 0.3); + } + .mode-option-card.selected { + background: rgba(102, 252, 241, 0.06); + border-color: #66fcf1; + } + .mode-radio { + accent-color: #66fcf1; + width: 18px; + height: 18px; + } + .start-btn { + background: #0284c7; + color: #fff; + border: none; + border-radius: 8px; + padding: 12px 24px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(2, 132, 199, 0.3); + width: 100%; + } + .start-btn:hover { + transform: scale(1.02); + background: #0369a1; + } + `; + document.head.appendChild(style); + } + + function showStartScreen() { + const quizBox = document.getElementById("quiz-box"); + if (!quizBox) return; + + // Hide quiz box initially + quizBox.style.display = "none"; + + // Create start screen + const startScreen = document.createElement("div"); + startScreen.id = "quiz-start-screen"; + startScreen.className = "quiz-start-card"; + + const topicId = getTopicIdFromPath(); + let topicLabel = "Practice Quiz"; + if (window.quizProgress && topicId) { + const t = window.quizProgress.QUIZ_TOPICS.find(x => x.id === topicId); + if (t) topicLabel = t.label; + } + + startScreen.innerHTML = ` +

${topicLabel} 🎯

+

Choose your quiz mode below to begin.

+ +
+ +
+ + Practice concepts in default difficulty progression. +
+
+ +
+ +
+ + Adapts questions dynamically to focus on your weakest concepts. +
+
+ + + `; + + quizBox.parentNode.insertBefore(startScreen, quizBox); + + const stdCard = startScreen.querySelector("#standard-mode-card"); + const weakCard = startScreen.querySelector("#weakness-mode-card"); + const stdRadio = startScreen.querySelector("#mode-standard"); + const weakRadio = startScreen.querySelector("#mode-weakness"); + + stdCard.addEventListener("click", () => { + stdRadio.checked = true; + stdCard.classList.add("selected"); + weakCard.classList.remove("selected"); + }); + + weakCard.addEventListener("click", () => { + weakRadio.checked = true; + weakCard.classList.add("selected"); + stdCard.classList.remove("selected"); + }); + + startScreen.querySelector("#start-quiz-action-btn").addEventListener("click", () => { + const isWeakness = weakRadio.checked; + window.isWeaknessFocusMode = isWeakness; + + startScreen.remove(); + quizBox.style.display = ""; + + if (isWeakness) { + if (window.questions && Array.isArray(window.questions) && window.quizProgress && typeof window.quizProgress.getQuestionWeaknessWeight === 'function') { + window.questions.sort((a, b) => { + const weightA = window.quizProgress.getQuestionWeaknessWeight(a); + const weightB = window.quizProgress.getQuestionWeaknessWeight(b); + return weightB - weightA; + }); + } + if (typeof window.restartQuiz === 'function') { + window.restartQuiz(); + } + } + }); + } + + function injectSessionSummary() { + const scoreEl = document.getElementById("score"); + if (!scoreEl) return; + + 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 || []; + } + + const improvedSkills = new Set(); + resolvedQuestions.forEach((q, idx) => { + const ans = resolvedAnswers[idx]; + if (ans !== undefined && ans !== null) { + const correctOption = typeof q.answer === 'number' && Array.isArray(q.options) ? q.options[q.answer] : q.answer; + const isCorrect = ans === correctOption; + if (isCorrect) { + const taxonomy = window.quizProgress?.SKILL_TAXONOMY; + const tax = taxonomy ? taxonomy[q.question?.trim()] : null; + if (tax) { + improvedSkills.add(tax.label); + } + } + } + }); + + let nextRecText = ""; + let nextRecUrl = ""; + if (window.quizProgress && typeof window.quizProgress.getWeakestSkills === 'function') { + const weak = window.quizProgress.getWeakestSkills({ limit: 1 }); + if (weak && weak.length > 0) { + nextRecText = weak[0].label; + nextRecUrl = weak[0].quizUrl; + } + } + + const summaryContainer = document.createElement("div"); + summaryContainer.className = "session-summary-card"; + summaryContainer.style.marginTop = "20px"; + summaryContainer.style.padding = "16px"; + summaryContainer.style.borderRadius = "12px"; + summaryContainer.style.background = "rgba(255, 255, 255, 0.03)"; + summaryContainer.style.border = "1px solid rgba(255, 255, 255, 0.08)"; + summaryContainer.style.textAlign = "left"; + + let improvedHtml = ""; + if (improvedSkills.size > 0) { + improvedHtml = ` +

+ Concepts Improved +

+ + `; + } else { + improvedHtml = ` +

+ No concepts improved this session. Keep practicing to level up! +

+ `; + } + + let recHtml = ""; + if (nextRecText) { + const path = window.location.pathname; + let prefix = "./"; + if (path.includes("/quiz/") || path.includes("/mathsquiz/") || path.includes("/chemistryquiz/") || path.includes("/sub/")) { + prefix = "../"; + } + const practiceUrl = nextRecUrl ? prefix + nextRecUrl : "#"; + + recHtml = ` +

+ Recommended Next Concept +

+

+ 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 @@

Teacher Progress Dashboard

β€”
β€”
+
+
Daily Goal
+
β€”
+
β€”
+
+

Accuracy over time

@@ -109,6 +115,30 @@

What to reteach (recommended)

+ +
+

+ 🎯 Personalized Concept Mastery +

+

Track mastery levels per specific sub-skill/concept. Recommendations adapt to the weakest areas.

+ +
+
+

Concept Mastery Details

+
+
No mastery data recorded yet.
+
+
+ +
+

Top Weak Concepts

+
+
No weak concepts identified yet.
+
+
+
+
+

Topic-wise performance

@@ -194,6 +224,7 @@

Active Assi +