diff --git a/dashboardProgress.js b/dashboardProgress.js new file mode 100644 index 0000000..243fc06 --- /dev/null +++ b/dashboardProgress.js @@ -0,0 +1,247 @@ +/** + * dashboardProgress.js — Parent/Teacher Progress Dashboard (read-only) + * + * Renders analytics using quizProgress.js localStorage data: + * - Accuracy trend chart (last 14 days) + * - Weak-topic recommendations + * - Topic-wise accuracy + attempts summary + * - Streak + overall accuracy KPI + * + * Works in demo mode (single learner) via localStorage. + */ + +(function () { + function pct(n) { + if (typeof n !== "number" || Number.isNaN(n)) return "—"; + return `${Math.round(n * 100)}%`; + } + + function formatAttempts(n) { + if (typeof n !== "number" || Number.isNaN(n)) return "0"; + return String(n); + } + + function drawLineChart(canvas, labels, accuracyByDay) { + const ctx = canvas.getContext("2d"); + const w = canvas.width; + const h = canvas.height; + + ctx.clearRect(0, 0, w, h); + + // Background grid + ctx.strokeStyle = "rgba(255,255,255,0.08)"; + ctx.lineWidth = 1; + for (let i = 1; i <= 4; i++) { + const y = (h / 5) * i; + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(w, y); + ctx.stroke(); + } + + const valid = accuracyByDay + .map((a, idx) => ({ a, idx })) + .filter(p => typeof p.a === "number" && !Number.isNaN(p.a)); + + if (valid.length < 2) { + ctx.fillStyle = "rgba(255,255,255,0.7)"; + ctx.font = "14px Arial"; + ctx.fillText("Complete at least 2 quiz attempts to see a trend.", 16, 28); + return; + } + + const xStep = w / (labels.length - 1); + const marginTop = 16; + const marginBottom = 24; + const usable = h - marginTop - marginBottom; + + const toY = (acc) => marginTop + (1 - acc) * usable; + + // Line + ctx.strokeStyle = "#66fcf1"; + ctx.lineWidth = 2; + ctx.beginPath(); + + for (let i = 0; i < accuracyByDay.length; i++) { + const a = accuracyByDay[i]; + if (typeof a !== "number" || Number.isNaN(a)) continue; + const x = i * xStep; + const y = toY(a); + if (ctx.__started !== true) { + ctx.moveTo(x, y); + ctx.__started = true; + } else { + ctx.lineTo(x, y); + } + } + + // Reset helper + delete ctx.__started; + + ctx.stroke(); + + // Points + valid.forEach(({ a, idx }) => { + const x = idx * xStep; + const y = toY(a); + ctx.fillStyle = "#66fcf1"; + ctx.beginPath(); + ctx.arc(x, y, 4, 0, 2 * Math.PI); + ctx.fill(); + }); + + // X labels + ctx.fillStyle = "rgba(255,255,255,0.65)"; + ctx.font = "12px Arial"; + const stride = Math.max(1, Math.floor(labels.length / 6)); + + labels.forEach((lab, i) => { + if (i % stride !== 0 && i !== labels.length - 1) return; + ctx.fillText(lab, i * xStep - 10, h - 8); + }); + } + + function ensureCanvasResolution(canvas, heightPx) { + if (!canvas) return; + const rect = canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + + // Use provided height if bbox is 0/undefined. + const targetW = rect.width || canvas.width || 600; + const targetH = rect.height || heightPx || 240; + + canvas.width = Math.floor(targetW * dpr); + canvas.height = Math.floor(targetH * dpr); + } + + function renderKpis(root) { + const streak = window.quizProgress?.getStreak?.(); + const overall = window.quizProgress?.getOverallAccuracy?.(); + + 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."; + } + + const overallAccuracyValue = root.querySelector("#overallAccuracyValue"); + const overallAccuracyMeta = root.querySelector("#overallAccuracyMeta"); + + if (overallAccuracyValue) overallAccuracyValue.textContent = overall?.accuracy == null ? "—" : pct(overall.accuracy); + if (overallAccuracyMeta) { + const correct = overall?.correct || 0; + const total = overall?.total || 0; + overallAccuracyMeta.textContent = total > 0 ? `${correct} correct out of ${total} answers` : "Complete a quiz to populate your stats."; + } + } + + function renderAccuracyChart(root) { + const series = window.quizProgress?.getAccuracySeries?.({ days: 14 }); + const canvas = root.querySelector("#accuracyChart"); + if (!canvas) return; + + if (!series || !series.labels || !Array.isArray(series.accuracyByDay)) { + const ctx = canvas.getContext("2d"); + ctx && ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + } + + ensureCanvasResolution(canvas, 240); + drawLineChart(canvas, series.labels, series.accuracyByDay); + } + + function renderRecommendations(root) { + const recEl = root.querySelector("#recommendedTopics"); + if (!recEl) return; + + const recs = window.quizProgress?.getRecommendedTopics?.({ limit: 3 }) || []; + recEl.innerHTML = ""; + + if (!recs.length) { + recEl.textContent = "No recommendations yet."; + return; + } + + recs.forEach(r => { + const chip = document.createElement("span"); + chip.className = "recommend-chip"; + const accText = r.accuracy == null ? "not attempted" : `accuracy ${pct(r.accuracy)}`; + chip.textContent = `${r.topic.label} • ${accText}`; + recEl.appendChild(chip); + }); + } + + function renderTopicStats(root) { + const topicStatsEl = root.querySelector("#topicStats"); + if (!topicStatsEl) return; + + const byTopic = window.quizProgress?.getAllTopicStats?.() || {}; + const topics = window.quizProgress?.QUIZ_TOPICS || []; + + const sorted = [...topics].sort((a, b) => { + const aAttempts = byTopic[a.id]?.attempts || 0; + const bAttempts = byTopic[b.id]?.attempts || 0; + return bAttempts - aAttempts; + }); + + topicStatsEl.innerHTML = ""; + + sorted.forEach(t => { + const agg = byTopic[t.id]; + const attempts = agg?.attempts || 0; + const qTotal = agg?.questionsTotal || 0; + const correctTotal = agg?.correctTotal || 0; + const accuracy = qTotal > 0 ? correctTotal / qTotal : null; + const barW = accuracy == null ? 0 : Math.max(0, Math.min(100, Math.round(accuracy * 100))); + + const row = document.createElement("div"); + row.className = "topic-row"; + row.innerHTML = ` +
${t.label}
+
+ +
+
+
${accuracy == null ? "—" : pct(accuracy)}
+
${formatAttempts(attempts)} attempts
+
+ `; + + topicStatsEl.appendChild(row); + }); + } + + function renderAll(root) { + // Guard: quizProgress must be loaded + if (!window.quizProgress) { + const status = root.querySelector("#dashboardStatus"); + if (status) status.textContent = "Progress data not available."; + return; + } + + renderKpis(root); + renderAccuracyChart(root); + renderRecommendations(root); + renderTopicStats(root); + } + + function initByRole() { + // Detect a container we can render into. + // parents.html / teachers.html will use the same structure. + const root = document.querySelector("[data-progress-dashboard='1']"); + if (!root) return; + + renderAll(root); + } + + document.addEventListener("DOMContentLoaded", () => { + initByRole(); + }); + + // Expose for debugging (optional) + window.dashboardProgress = { renderAll, initByRole }; +})(); + diff --git a/parents.html b/parents.html index f04d6fc..3b9e425 100644 --- a/parents.html +++ b/parents.html @@ -54,12 +54,51 @@

Support for Parents

-

Why LearnSphere?

-

We offer a variety of courses designed to help you excel in academics and beyond.

- +

Parent Progress Dashboard

+

Accuracy trends, weak topics, and recommended next practice — read-only demo mode.

+
+ +
+
+
+
+
Current Streak
+
+
+
+
+
Overall Accuracy
+
+
+
+
+ +
+
+

Accuracy over time

+ +
Based on last 14 days of quiz attempts.
+
+ +
+

Weak topics (recommended)

+
+
We prioritize topics with lower accuracy or fewer attempts.
+
+
+ +
+

Topic-wise performance

+
+
Shows accuracy + attempts per topic.
+
+
+ + + diff --git a/quiz/adaptiveQuiz.js b/quiz/adaptiveQuiz.js new file mode 100644 index 0000000..3d8bed8 --- /dev/null +++ b/quiz/adaptiveQuiz.js @@ -0,0 +1,145 @@ +/** + * adaptiveQuiz.js + * Shared helper for Adaptive Quiz Difficulty (client-side only). + * + * Usage (per quiz page JS): + * import/require is not used here (no bundler). Include this file via: + * + * Then call: + * const adaptive = buildAdaptiveQuiz({ questions }); + */ + +(function () { + 'use strict'; + + function normalizeDifficulty(d) { + const val = String(d || '').toLowerCase(); + if (val === 'easy' || val === 'e') return 0; + if (val === 'medium' || val === 'med' || val === 'm') return 1; + if (val === 'hard' || val === 'h') return 2; + + // Default: medium + return 1; + } + + function clamp(n, min, max) { + return Math.max(min, Math.min(max, n)); + } + + /** + * Build a per-difficulty bucket of question copies. + * @param {Array<{difficulty?: string|number}>} questions + */ + function buildBuckets(questions) { + const buckets = { 0: [], 1: [], 2: [] }; + questions.forEach((q, idx) => { + const di = normalizeDifficulty(q.difficulty); + // keep original index for tracking + buckets[di].push({ ...q, __qid: idx }); + }); + + // Shuffle each bucket for variety. + Object.keys(buckets).forEach(k => { + const arr = buckets[k]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + }); + + return buckets; + } + + function createAdaptiveQuiz({ questions, startingDifficultyIndex = 1 }) { + const buckets = buildBuckets(questions); + const totalSteps = questions.length; + + let difficultyIndex = clamp(startingDifficultyIndex, 0, 2); + let consecutiveCorrect = 0; + let consecutiveIncorrect = 0; + + // Flattened remaining counts for quick fallback. + function countRemainingAt(di) { + return buckets[di].length; + } + + function totalRemaining() { + return buckets[0].length + buckets[1].length + buckets[2].length; + } + + /** + * Pick next question from the current difficulty if possible. + * Otherwise, fallback to nearest difficulty that still has questions. + */ + function takeNext() { + if (totalRemaining() <= 0) return null; + + // Try current difficulty first. + if (countRemainingAt(difficultyIndex) > 0) { + return buckets[difficultyIndex].shift(); + } + + // Fallback: nearest difficulty. + for (let dist = 1; dist <= 2; dist++) { + const lower = difficultyIndex - dist; + const upper = difficultyIndex + dist; + if (lower >= 0 && countRemainingAt(lower) > 0) return buckets[lower].shift(); + if (upper <= 2 && countRemainingAt(upper) > 0) return buckets[upper].shift(); + } + + // Final fallback: any remaining. + for (let di = 0; di <= 2; di++) { + if (countRemainingAt(di) > 0) return buckets[di].shift(); + } + + return null; + } + + /** + * Update difficulty after an answer. + */ + function updateDifficulty({ isCorrect }) { + if (isCorrect) { + consecutiveCorrect += 1; + consecutiveIncorrect = 0; + + // After 2 consecutive correct: go harder. + if (consecutiveCorrect >= 2) { + difficultyIndex = clamp(difficultyIndex + 1, 0, 2); + consecutiveCorrect = 0; + } + } else { + consecutiveIncorrect += 1; + consecutiveCorrect = 0; + + // After 2 consecutive incorrect: go easier. + if (consecutiveIncorrect >= 2) { + difficultyIndex = clamp(difficultyIndex - 1, 0, 2); + consecutiveIncorrect = 0; + } + } + + return difficultyIndex; + } + + return { + takeNext, + updateDifficulty, + getDifficultyIndex: () => difficultyIndex, + getTotalSteps: () => totalSteps, + }; + } + + function getStartingDifficultyFromAccuracy({ accuracy }) { + if (typeof accuracy !== 'number' || !isFinite(accuracy)) return 1; // medium + if (accuracy >= 0.8) return 2; // hard + if (accuracy <= 0.5) return 0; // easy + return 1; // medium + } + + // Export to window. + window.createAdaptiveQuiz = createAdaptiveQuiz; + window.getStartingDifficultyFromAccuracy = getStartingDifficultyFromAccuracy; + +})(); + diff --git a/quiz/motionquiz.html b/quiz/motionquiz.html index d2cca23..ae7b58a 100644 --- a/quiz/motionquiz.html +++ b/quiz/motionquiz.html @@ -63,8 +63,11 @@

🎉 Quiz Completed!

+ + + + + + \ No newline at end of file diff --git a/quiz/projectilequiz.js b/quiz/projectilequiz.js index 24a7ba5..bca88b4 100644 --- a/quiz/projectilequiz.js +++ b/quiz/projectilequiz.js @@ -1,15 +1,18 @@ const questions = [ - { question: "What is the path of a projectile in ideal conditions?", options: ["Straight line", "Circular", "Parabolic", "Elliptical"], answer: "Parabolic" }, - { question: "What is the horizontal acceleration of a projectile in the absence of air resistance?", options: ["9.8 m/s²", "0 m/s²", "Depends on initial velocity", "Constant"], answer: "0 m/s²" }, - { question: "At the highest point of its trajectory, what is the vertical velocity of a projectile?", options: ["Maximum", "Zero", "Equal to initial velocity", "Depends on mass"], answer: "Zero" }, - { question: "Which factor affects the range of a projectile the most?", options: ["Mass", "Launch angle", "Time of flight", "Shape"], answer: "Launch angle" }, - { question: "What is the optimal angle for maximum range in projectile motion (neglecting air resistance)?", options: ["30°", "45°", "60°", "90°"], answer: "45°" } + { difficulty: "easy", question: "What is the path of a projectile in ideal conditions?", options: ["Straight line", "Circular", "Parabolic", "Elliptical"], answer: "Parabolic" }, + { difficulty: "easy", question: "What is the horizontal acceleration of a projectile in the absence of air resistance?", options: ["9.8 m/s²", "0 m/s²", "Depends on initial velocity", "Constant"], answer: "0 m/s²" }, + { difficulty: "medium", question: "At the highest point of its trajectory, what is the vertical velocity of a projectile?", options: ["Maximum", "Zero", "Equal to initial velocity", "Depends on mass"], answer: "Zero" }, + { difficulty: "medium", question: "Which factor affects the range of a projectile the most?", options: ["Mass", "Launch angle", "Time of flight", "Shape"], answer: "Launch angle" }, + { difficulty: "hard", question: "What is the optimal angle for maximum range in projectile motion (neglecting air resistance)?", options: ["30°", "45°", "60°", "90°"], answer: "45°" } ]; -let currentQuestionIndex = 0; +let adaptiveQuiz = null; +let adaptiveSteps = []; +let currentStepIndex = 0; let score = 0; let selectedOption = null; -let userAnswers = new Array(questions.length).fill(null); +let userSelectionsByStep = []; + function loadQuestion() { let questionData = questions[currentQuestionIndex]; diff --git a/quiz/rayquiz.html b/quiz/rayquiz.html index 3e700ec..cd294ad 100644 --- a/quiz/rayquiz.html +++ b/quiz/rayquiz.html @@ -51,7 +51,9 @@

🎉 Quiz Completed!

+ + \ No newline at end of file diff --git a/teachers.html b/teachers.html index 3026d0f..aca23ae 100644 --- a/teachers.html +++ b/teachers.html @@ -54,12 +54,51 @@

Empowering Teachers

-

Why LearnSphere?

-

We offer a variety of courses designed to help you excel in academics and beyond.

- +

Teacher Progress Dashboard

+

Accuracy trends and weak-topic insights — read-only demo mode.

+
+ +
+
+
+
+
Current Streak
+
+
+
+
+
Overall Accuracy
+
+
+
+
+ +
+
+

Accuracy over time

+ +
Last 14 days of attempts.
+
+ +
+

What to reteach (recommended)

+
+
Lower accuracy topics appear first.
+
+
+ +
+

Topic-wise performance

+
+
Shows accuracy + attempts per topic (read-only).
+
+
+ + +