diff --git a/accessibility.js b/accessibility.js new file mode 100644 index 0000000..80cedcd --- /dev/null +++ b/accessibility.js @@ -0,0 +1,110 @@ +// accessibility.js - Core utilities for quiz accessibility and keyboard navigation +// ------------------------------------------------------------ +// This script provides functions to initialize ARIA roles, manage focus, and handle +// keyboard shortcuts for quiz pages. It attaches its API to the global `window` object. + +(function () { + // Initialize accessibility for a quiz. + function initQuizAccessibility({ containerId = 'quiz-box', optionsContainerId = 'options', statusId = 'sr-status' } = {}) { + const container = document.getElementById(containerId); + const optionsContainer = document.getElementById(optionsContainerId); + const srStatus = document.getElementById(statusId); + + // Ensure the options container has proper ARIA role (radiogroup) if not already set. + if (optionsContainer && !optionsContainer.getAttribute('role')) { + optionsContainer.setAttribute('role', 'radiogroup'); + } + + // Attach a keydown listener for navigation shortcuts. + document.addEventListener('keydown', (e) => handleKeyNavigation(e, optionsContainer, srStatus)); + + return { container, optionsContainer, srStatus }; + } + + // Keyboard navigation handler. + function handleKeyNavigation(event, optionsContainer, srStatus) { + const KEY = { + LEFT: 'ArrowLeft', + RIGHT: 'ArrowRight', + ENTER: 'Enter', + SPACE: ' ', + CTRL_ENTER: 'Enter', + }; + + const activeEl = document.activeElement; + const isOption = activeEl && activeEl.classList && activeEl.classList.contains('option'); + + if (event.key === KEY.LEFT && isOption) { + focusPrevOption(activeEl); + event.preventDefault(); + return; + } + if (event.key === KEY.RIGHT && isOption) { + focusNextOption(activeEl); + event.preventDefault(); + return; + } + if ((event.key === KEY.ENTER || event.key === KEY.SPACE) && isOption) { + activeEl.click(); + event.preventDefault(); + return; + } + // Ctrl+Enter to submit current answer (if submit button is visible). + if (event.key === KEY.CTRL_ENTER && event.ctrlKey) { + const submitBtn = document.getElementById('submit-btn'); + if (submitBtn && !submitBtn.classList.contains('hidden')) { + submitBtn.click(); + event.preventDefault(); + } + return; + } + } + + function focusPrevOption(current) { + const options = Array.from(document.querySelectorAll('.option')); + const idx = options.indexOf(current); + if (idx > 0) { + options[idx - 1].focus(); + announceOptionChange(options[idx - 1]); + } + } + + function focusNextOption(current) { + const options = Array.from(document.querySelectorAll('.option')); + const idx = options.indexOf(current); + if (idx < options.length - 1) { + options[idx + 1].focus(); + announceOptionChange(options[idx + 1]); + } + } + + function announceOptionChange(optionEl) { + const sr = document.getElementById('sr-status'); + if (sr) { + sr.textContent = `Option ${optionEl.textContent.trim()} selected`; + } + } + + function announce(message) { + const sr = document.getElementById('sr-status'); + if (sr) { + sr.textContent = ''; + setTimeout(() => { + sr.textContent = message; + }, 0); + } + } + + // Detect reduced motion preference and add a class to the body. + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + document.body.classList.add('prefers-reduced-motion'); + } + // Detect high‑contrast preference (CSS Level 4 media query) and add a class. + if (window.matchMedia('(prefers-contrast: more)').matches) { + document.body.classList.add('high-contrast'); + } + + // Expose API globally. + window.initQuizAccessibility = initQuizAccessibility; + window.srAnnounce = announce; +})(); diff --git a/class_manager.js b/class_manager.js new file mode 100644 index 0000000..6ca3629 --- /dev/null +++ b/class_manager.js @@ -0,0 +1,85 @@ +// class_manager.js – Handles teacher class sessions and progress aggregation + +const CLASS_SESSIONS_KEY = 'learnsphere_class_sessions_v1'; + +function _loadClasses() { + try { + const raw = localStorage.getItem(CLASS_SESSIONS_KEY); + return raw ? JSON.parse(raw) : []; + } catch (e) { + console.warn('LearnSphere: Failed to load class sessions', e); + return []; + } +} + +function _saveClasses(classes) { + try { + localStorage.setItem(CLASS_SESSIONS_KEY, JSON.stringify(classes)); + } catch (e) { + console.warn('LearnSphere: Failed to save class sessions', e); + } +} + +function generateInviteCode(length = 6) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let code = ''; + for (let i = 0; i < length; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; +} + +function createClassSession({ name, topics = [], dueDate }) { + const classes = _loadClasses(); + const id = 'cls_' + Date.now(); + const inviteCode = generateInviteCode(); + const newClass = { + id, + name, + inviteCode, + topics, + dueDate: dueDate || null, + attempts: [], // each { studentId, quizId, score } + createdAt: new Date().toISOString() + }; + classes.push(newClass); + _saveClasses(classes); + return newClass; +} + +function recordClassAttempt(classId, { studentId = 'anonymous', quizId, score }) { + if (!classId) return; + const classes = _loadClasses(); + const cls = classes.find(c => c.id === classId); + if (!cls) return; + cls.attempts.push({ studentId, quizId, score, timestamp: Date.now() }); + _saveClasses(classes); +} + +function getClassStats(classId) { + const classes = _loadClasses(); + const cls = classes.find(c => c.id === classId); + if (!cls) return null; + const total = cls.attempts.length; + const avgScore = total ? (cls.attempts.reduce((a, b) => a + b.score, 0) / total).toFixed(2) : '-'; + const bestScore = total ? Math.max(...cls.attempts.map(a => a.score)).toFixed(2) : '-'; + return { ...cls, totalAttempts: total, avgScore, bestScore }; +} + +function getAllClassStats() { + const classes = _loadClasses(); + return classes.map(c => { + const total = c.attempts.length; + const avgScore = total ? (c.attempts.reduce((a, b) => a + b.score, 0) / total).toFixed(2) : '-'; + const bestScore = total ? Math.max(...c.attempts.map(a => a.score)).toFixed(2) : '-'; + return { id: c.id, name: c.name, inviteCode: c.inviteCode, totalAttempts: total, avgScore, bestScore }; + }); +} + +// Expose globally for other modules +window.classManager = { + createClassSession, + recordClassAttempt, + getClassStats, + getAllClassStats +}; diff --git a/exportProgress.js b/exportProgress.js index e44f6b4..f8efb6d 100644 --- a/exportProgress.js +++ b/exportProgress.js @@ -374,12 +374,80 @@ a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 5000); } + // Snapshot utilities + async function generateSnapshot() { + const payload = buildProgressExportPayload({ roleContext: "learner" }); + const exportId = `snapshot_${Date.now()}_${Math.random().toString(16).slice(2)}`; + const encoder = new TextEncoder(); + const data = encoder.encode(JSON.stringify(payload)); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const payloadHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + const snapshot = { payload, payloadHash, generatedAt: new Date().toISOString() }; + localStorage.setItem(`snapshot_${exportId}`, JSON.stringify(snapshot)); + const index = JSON.parse(localStorage.getItem('snapshotIndex') || '[]'); + index.push({ exportId, generatedAt: snapshot.generatedAt, payloadHash }); + localStorage.setItem('snapshotIndex', JSON.stringify(index)); + return exportId; + } + + function downloadSnapshotAsImage(exportId) { + const snapshotStr = localStorage.getItem(`snapshot_${exportId}`); + if (!snapshotStr) { alert('Snapshot not found'); return; } + const snapshot = JSON.parse(snapshotStr); + const html = `
${JSON.stringify(snapshot.payload, null, 2)}
`; + const iframe = document.createElement('iframe'); + iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; + document.body.appendChild(iframe); + iframe.contentDocument.open(); iframe.contentDocument.write(html); iframe.contentDocument.close(); + setTimeout(() => { + html2canvas(iframe.contentDocument.body).then(canvas => { + const link = document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = `${exportId}.png`; + link.click(); + iframe.remove(); + }); + }, 500); + } + + function downloadSnapshotAsPDF(exportId) { + const snapshotStr = localStorage.getItem(`snapshot_${exportId}`); + if (!snapshotStr) { alert('Snapshot not found'); return; } + const snapshot = JSON.parse(snapshotStr); + const html = `
${JSON.stringify(snapshot.payload, null, 2)}
`; + const iframe = document.createElement('iframe'); + iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; + document.body.appendChild(iframe); + iframe.contentDocument.open(); iframe.contentDocument.write(html); iframe.contentDocument.close(); + setTimeout(() => { + html2canvas(iframe.contentDocument.body).then(canvas => { + const imgData = canvas.toDataURL('image/png'); + const pdf = new jspdf.jsPDF(); + const imgProps = pdf.getImageProperties(imgData); + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; + pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); + pdf.save(`${exportId}.pdf`); + iframe.remove(); + }); + }, 500); + } + + function listSnapshots() { + return JSON.parse(localStorage.getItem('snapshotIndex') || '[]'); + } + window.exportProgress = { buildProgressExportPayload, downloadJson, - }; -})(); + generateSnapshot, + downloadSnapshotAsImage, + downloadSnapshotAsPDF, + listSnapshots + };})(); diff --git a/my_progress.html b/my_progress.html index da99ddb..263b028 100644 --- a/my_progress.html +++ b/my_progress.html @@ -217,10 +217,13 @@

Export Progress

+
+ + + diff --git a/quiz/nlmquiz.js b/quiz/nlmquiz.js index b935827..32a5e72 100644 --- a/quiz/nlmquiz.js +++ b/quiz/nlmquiz.js @@ -325,11 +325,13 @@ function startAdaptiveSession() { // Remaining questions will be determined as user answers. } -document.addEventListener("DOMContentLoaded", () => { +document.addEventListener('DOMContentLoaded', () => { + // Initialize accessibility utilities + if (window.initQuizAccessibility) { + window.initQuizAccessibility(); + } window.__quizNlmStartedAt = Date.now(); - document.getElementById("progress-bar").style.width = "0%"; + document.getElementById('progress-bar').style.width = "0%"; startAdaptiveSession(); loadQuestion(); -}); - - +}); diff --git a/quiz/projectilequiz.html b/quiz/projectilequiz.html index f3d24a4..5db9b12 100644 --- a/quiz/projectilequiz.html +++ b/quiz/projectilequiz.html @@ -68,6 +68,7 @@

🎉 Quiz Completed!

+ diff --git a/quiz/projectilequiz.js b/quiz/projectilequiz.js index 17f43d7..0f8b0fa 100644 --- a/quiz/projectilequiz.js +++ b/quiz/projectilequiz.js @@ -284,6 +284,10 @@ document.addEventListener("keydown", (e) => { }); document.addEventListener("DOMContentLoaded", () => { + // Initialize accessibility utilities + if (window.initQuizAccessibility) { + window.initQuizAccessibility(); + } document.getElementById("progress-bar").style.width = "0%"; loadQuestion(); diff --git a/quiz/rayquiz.html b/quiz/rayquiz.html index 205324f..29752b8 100644 --- a/quiz/rayquiz.html +++ b/quiz/rayquiz.html @@ -68,6 +68,7 @@

🎉 Quiz Completed!

+ diff --git a/quiz/rayquiz.js b/quiz/rayquiz.js index 6aff0db..d9279e5 100644 --- a/quiz/rayquiz.js +++ b/quiz/rayquiz.js @@ -275,6 +275,10 @@ document.addEventListener("keydown", (e) => { }); document.addEventListener("DOMContentLoaded", () => { + // Initialize accessibility utilities + if (window.initQuizAccessibility) { + window.initQuizAccessibility(); + } document.getElementById("progress-bar").style.width = "0%"; loadQuestion(); diff --git a/review.js b/review.js index deff799..6a70aed 100644 --- a/review.js +++ b/review.js @@ -554,18 +554,63 @@ const submitBtn = document.getElementById("reviewSubmitBtn"); if (submitBtn) { submitBtn.onclick = function () { + // Compute score and update summary const { scorePct, correctCount, total, answeredCount } = readQuizAnswers(); const msg = document.getElementById("reviewResultMessage"); if (msg) msg.textContent = `Score: ${correctCount}/${total} (${scorePct}%).`; + // Render detailed per‑question results with an Ask button + const container = document.getElementById("reviewQuizContainer"); + if (container) { + container.innerHTML = ""; + const quiz = getReviewQuiz(topicId); + quiz.forEach((item, idx) => { + const qDiv = document.createElement("div"); + qDiv.className = "review-question-result"; + qDiv.style.marginBottom = "12px"; + + const selected = document.querySelector(`input[name="review_q_${idx}"]:checked`); + const userIdx = selected ? Number(selected.value) : null; + const userAns = userIdx !== null ? item.options[userIdx] : "No answer"; + const correct = userIdx === item.answerIndex; + + qDiv.innerHTML = ` +
Q${idx + 1}: ${item.q}
+
Your answer: ${userAns} ${correct ? "✅" : "❌"}
+
Correct answer: ${item.options[item.answerIndex]}
+ `; + + const askBtn = document.createElement("button"); + askBtn.textContent = "Ask about this question"; + askBtn.className = "action-btn"; + askBtn.style.marginTop = "6px"; + askBtn.onclick = () => { + fetch("/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: `Help me understand this question: ${item.q}` }) + }) + .then(res => res.json()) + .then(data => { + alert(data.reply || "No reply"); + }) + .catch(err => { + console.error(err); + alert("Error contacting chatbot"); + }); + }; + qDiv.appendChild(askBtn); + container.appendChild(qDiv); + }); + } + + // Persist the review result recordReviewResult({ topicId, scorePct, answeredCount }); - // Disable button while closing + // Disable button to prevent double submission submitBtn.disabled = true; - setTimeout(() => { closeModal(); - // Dispatch event to refresh lists in real time window.dispatchEvent(new Event("review-saved")); }, 800); }; diff --git a/teachers.html b/teachers.html index 34daea8..b1c6da6 100644 --- a/teachers.html +++ b/teachers.html @@ -72,6 +72,8 @@

Empowering Teachers

Teacher guiding students + +
diff --git a/weeklyReport.js b/weeklyReport.js new file mode 100644 index 0000000..e446d78 --- /dev/null +++ b/weeklyReport.js @@ -0,0 +1,118 @@ +// weeklyReport.js — Parent Weekly Summary +// Generates a concise weekly overview: streak, top strengths, and areas to improve. +// Assumes window.quizProgress and window.studyProgress are already loaded. + +(function () { + /** Helper: map skillId → human readable label */ + const _skillLabelMap = (() => { + const map = {}; + if (window.quizProgress && window.quizProgress.SKILL_TAXONOMY) { + Object.values(window.quizProgress.SKILL_TAXONOMY).forEach(t => { + if (t && t.skillId) map[t.skillId] = t.label; + }); + } + return map; + })(); + + function _updateStreak() { + const streakEl = document.getElementById('streakValue'); + if (!streakEl) return; + let streak = null; + if (window.studyProgress && typeof window.studyProgress.loadStreakState === 'function') { + const state = window.studyProgress.loadStreakState(); + streak = state.currentStreak; + } else if (window.quizProgress && typeof window.quizProgress.getStreak === 'function') { + streak = window.quizProgress.getStreak().currentStreak; + } + streakEl.textContent = streak !== null && streak !== undefined ? streak : '—'; + } + + /** Render top strength skills (high accuracy, enough attempts) */ + function _renderStrengths() { + const container = document.getElementById('masterySkillsList'); + if (!container) return; + const mastery = (window.quizProgress && typeof window.quizProgress.getMasteryStats === 'function') + ? window.quizProgress.getMasteryStats() + : {}; + const strengths = Object.entries(mastery) + .filter(([, m]) => m.attempts && m.attempts >= 3) + .map(([skillId, m]) => ({ + skillId, + attempts: m.attempts, + correct: m.correct, + accuracy: m.attempts ? m.correct / m.attempts : 0, + label: _skillLabelMap[skillId] || skillId, + })) + .sort((a, b) => b.accuracy - a.accuracy) + .slice(0, 3); + + container.innerHTML = ''; + if (strengths.length === 0) { + const empty = document.createElement('div'); + empty.textContent = 'No strength data yet.'; + container.appendChild(empty); + return; + } + strengths.forEach(s => { + const card = document.createElement('div'); + card.style.display = 'flex'; + card.style.justifyContent = 'space-between'; + card.style.padding = '4px 0'; + + const label = document.createElement('span'); + label.textContent = s.label; + label.style.fontWeight = '500'; + const info = document.createElement('span'); + const pct = Math.round(s.accuracy * 100); + info.textContent = `${pct}% (${s.attempts} attempts)`; + info.style.color = 'var(--text-muted)'; + + card.appendChild(label); + card.appendChild(info); + container.appendChild(card); + }); + } + + /** Render weak concepts (recommended focus) */ + function _renderWeaknesses() { + const container = document.getElementById('weakSkillsRecommendations'); + if (!container) return; + const weak = (window.quizProgress && typeof window.quizProgress.getWeakestSkills === 'function') + ? window.quizProgress.getWeakestSkills({ limit: 3 }) + : []; + + container.innerHTML = ''; + if (weak.length === 0) { + const empty = document.createElement('div'); + empty.textContent = 'No weak concepts identified yet.'; + container.appendChild(empty); + return; + } + weak.forEach(w => { + const card = document.createElement('div'); + card.style.display = 'flex'; + card.style.justifyContent = 'space-between'; + card.style.padding = '4px 0'; + + const label = document.createElement('span'); + label.textContent = w.label; + label.style.fontWeight = '500'; + const info = document.createElement('span'); + const pct = w.accuracy !== null ? Math.round(w.accuracy * 100) : 'N/A'; + info.textContent = `${pct}% (${w.attempts} attempts)`; + info.style.color = 'var(--text-muted)'; + + card.appendChild(label); + card.appendChild(info); + container.appendChild(card); + }); + } + + function _renderWeeklyReport() { + _updateStreak(); + _renderStrengths(); + _renderWeaknesses(); + } + + document.addEventListener('DOMContentLoaded', _renderWeeklyReport); +})();