Spaces:
Running
Running
Download app.js from matt-ritchey/math4: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/spaces/matt-ritchey/math4/resolve/main/app.js
- Command line
-
hf download hf://spaces/matt-ritchey/math4/app.js
-
curl -L -o app.js https://huggingface.co/spaces/matt-ritchey/math4/resolve/main/app.js
13.1 kB
| const state = { | |
| selectedTopic: null, | |
| selectedDifficulty: 1, | |
| selectedCount: 10, | |
| practice: { questions: [], index: 0, awaitingNext: false, currentProblem: null, topic: null, difficulty: 1, count: 0 }, | |
| quiz: { questions: [], index: 0, score: 0, timer: null, timeLeft: 0, active: false } | |
| }; | |
| function navigate(view) { | |
| if (view === 'home' && state.quiz.active) { | |
| clearInterval(state.quiz.timer); | |
| state.quiz.active = false; | |
| } | |
| document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); | |
| const el = document.getElementById('view-' + view); | |
| if (el) el.classList.add('active'); | |
| updateHeader(); | |
| if (view === 'progress') renderProgress(); | |
| if (view === 'home') renderHome(); | |
| if (view === 'topics' || view === 'quiz-setup') resetSelection(view); | |
| window.scrollTo(0, 0); | |
| } | |
| function updateHeader() { | |
| const data = ProgressStore.load(); | |
| document.getElementById('points-header').textContent = data.points; | |
| document.getElementById('streak-header').textContent = data.streak; | |
| } | |
| function renderHome() { | |
| updateHeader(); | |
| } | |
| function resetSelection(view) { | |
| const container = document.getElementById('view-' + view); | |
| container.querySelectorAll('.topic-card').forEach(c => c.classList.remove('selected')); | |
| container.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active')); | |
| container.querySelector('.diff-btn[data-level="1"]')?.classList.add('active'); | |
| state.selectedTopic = null; | |
| state.selectedDifficulty = 1; | |
| const startBtn = document.getElementById(view === 'topics' ? 'start-practice' : 'start-quiz'); | |
| if (startBtn) startBtn.disabled = true; | |
| } | |
| document.addEventListener('click', function(e) { | |
| const topicCard = e.target.closest('.topic-card'); | |
| if (topicCard) { | |
| const view = topicCard.closest('.view'); | |
| if (!view) return; | |
| view.querySelectorAll('.topic-card').forEach(c => c.classList.remove('selected')); | |
| topicCard.classList.add('selected'); | |
| state.selectedTopic = topicCard.dataset.topic; | |
| const isQuiz = view.id === 'view-quiz-setup'; | |
| const btn = document.getElementById(isQuiz ? 'start-quiz' : 'start-practice'); | |
| if (btn) btn.disabled = false; | |
| return; | |
| } | |
| const diffBtn = e.target.closest('.diff-btn'); | |
| if (diffBtn) { | |
| const view = diffBtn.closest('.view'); | |
| view.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active')); | |
| diffBtn.classList.add('active'); | |
| state.selectedDifficulty = parseInt(diffBtn.dataset.level); | |
| return; | |
| } | |
| const countBtn = e.target.closest('.count-btn'); | |
| if (countBtn) { | |
| countBtn.closest('.quiz-count-selector').querySelectorAll('.count-btn').forEach(b => b.classList.remove('active')); | |
| countBtn.classList.add('active'); | |
| state.selectedCount = parseInt(countBtn.dataset.count); | |
| return; | |
| } | |
| }); | |
| document.getElementById('start-practice')?.addEventListener('click', function() { | |
| if (!state.selectedTopic) return; | |
| startPractice(); | |
| }); | |
| document.getElementById('start-quiz')?.addEventListener('click', function() { | |
| if (!state.selectedTopic) return; | |
| startQuiz(); | |
| }); | |
| function startPractice() { | |
| state.practice.topic = state.selectedTopic; | |
| state.practice.difficulty = state.selectedDifficulty; | |
| state.practice.count = 0; | |
| nextPracticeProblem(); | |
| } | |
| function nextPracticeProblem() { | |
| const topic = state.practice.topic; | |
| const diff = state.practice.difficulty; | |
| const problem = MathEngine.generateProblem(topic, diff); | |
| state.practice.currentProblem = problem; | |
| state.practice.awaitingNext = false; | |
| state.practice.count++; | |
| navigate('practice'); | |
| showPracticeProblem(); | |
| } | |
| function showPracticeProblem() { | |
| const q = state.practice.currentProblem; | |
| document.getElementById('practice-problem').textContent = q.display; | |
| document.getElementById('practice-counter').textContent = `#${state.practice.count}`; | |
| document.getElementById('practice-topic-label').textContent = q.type.replace('-', ' '); | |
| document.getElementById('practice-input').value = ''; | |
| document.getElementById('practice-input').disabled = false; | |
| document.getElementById('practice-check').disabled = false; | |
| document.getElementById('practice-feedback').className = ''; | |
| document.getElementById('practice-feedback').textContent = ''; | |
| document.getElementById('practice-steps').style.display = 'none'; | |
| document.getElementById('practice-steps').innerHTML = ''; | |
| document.getElementById('practice-actions').style.display = 'none'; | |
| document.getElementById('practice-input').focus(); | |
| } | |
| document.getElementById('practice-check')?.addEventListener('click', checkPracticeAnswer); | |
| document.getElementById('practice-input')?.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| if (state.practice.awaitingNext) { | |
| nextPracticeProblem(); | |
| } else { | |
| checkPracticeAnswer(); | |
| } | |
| } | |
| }); | |
| function checkPracticeAnswer() { | |
| if (state.practice.awaitingNext) return; | |
| const input = document.getElementById('practice-input'); | |
| const answer = parseFloat(input.value); | |
| const problem = state.practice.currentProblem; | |
| if (isNaN(answer)) return; | |
| const correct = answer === problem.answer; | |
| const fb = document.getElementById('practice-feedback'); | |
| const steps = document.getElementById('practice-steps'); | |
| if (correct) { | |
| fb.className = 'feedback-correct'; | |
| fb.textContent = 'โ Correct!'; | |
| } else { | |
| fb.className = 'feedback-incorrect'; | |
| fb.textContent = `โ Not quite. The answer is ${problem.answerDisplay || problem.answer}`; | |
| } | |
| ProgressStore.recordAnswer(problem.type, correct); | |
| updateHeader(); | |
| input.disabled = true; | |
| document.getElementById('practice-check').disabled = true; | |
| state.practice.awaitingNext = true; | |
| document.getElementById('practice-actions').style.display = 'flex'; | |
| } | |
| document.getElementById('practice-show-steps')?.addEventListener('click', function() { | |
| const problem = state.practice.currentProblem; | |
| const steps = document.getElementById('practice-steps'); | |
| if (steps.style.display === 'none') { | |
| steps.innerHTML = problem.steps.map(s => `<p>${s}</p>`).join(''); | |
| steps.style.display = 'block'; | |
| this.textContent = 'Hide Steps'; | |
| } else { | |
| steps.style.display = 'none'; | |
| this.textContent = 'Show Steps'; | |
| } | |
| }); | |
| document.getElementById('practice-next')?.addEventListener('click', function() { | |
| nextPracticeProblem(); | |
| }); | |
| function startQuiz() { | |
| const topic = state.selectedTopic; | |
| const diff = state.selectedDifficulty; | |
| const count = state.selectedCount; | |
| state.quiz.questions = MathEngine.generateQuizQuestions(topic, diff, count); | |
| state.quiz.index = 0; | |
| state.quiz.score = 0; | |
| state.quiz.timeLeft = count * 15; | |
| state.quiz.active = true; | |
| navigate('quiz'); | |
| showQuizProblem(); | |
| startQuizTimer(); | |
| } | |
| function showQuizProblem() { | |
| const q = state.quiz.questions[state.quiz.index]; | |
| document.getElementById('quiz-problem').textContent = q.display; | |
| document.getElementById('quiz-question-num').textContent = `Question ${state.quiz.index + 1} of ${state.quiz.questions.length}`; | |
| document.getElementById('quiz-score').textContent = state.quiz.score; | |
| document.getElementById('quiz-total').textContent = state.quiz.questions.length; | |
| document.getElementById('quiz-input').value = ''; | |
| document.getElementById('quiz-input').disabled = false; | |
| document.getElementById('quiz-submit').disabled = false; | |
| document.getElementById('quiz-feedback').className = ''; | |
| document.getElementById('quiz-feedback').textContent = ''; | |
| document.getElementById('quiz-input').focus(); | |
| updateQuizProgress(); | |
| } | |
| function updateQuizProgress() { | |
| const pct = (state.quiz.index / state.quiz.questions.length) * 100; | |
| document.getElementById('quiz-progress-fill').style.width = pct + '%'; | |
| } | |
| function startQuizTimer() { | |
| updateTimerDisplay(); | |
| state.quiz.timer = setInterval(function() { | |
| state.quiz.timeLeft--; | |
| updateTimerDisplay(); | |
| if (state.quiz.timeLeft <= 0) { | |
| clearInterval(state.quiz.timer); | |
| endQuiz(); | |
| } | |
| }, 1000); | |
| } | |
| function updateTimerDisplay() { | |
| const minutes = Math.floor(state.quiz.timeLeft / 60); | |
| const seconds = state.quiz.timeLeft % 60; | |
| document.getElementById('quiz-timer').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| const timerEl = document.querySelector('.quiz-timer'); | |
| if (state.quiz.timeLeft <= 15) { | |
| timerEl.style.color = '#e74c3c'; | |
| } else if (state.quiz.timeLeft <= 30) { | |
| timerEl.style.color = '#f5a623'; | |
| } else { | |
| timerEl.style.color = '#2c3e50'; | |
| } | |
| } | |
| document.getElementById('quiz-submit')?.addEventListener('click', submitQuizAnswer); | |
| document.getElementById('quiz-input')?.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') submitQuizAnswer(); | |
| }); | |
| function submitQuizAnswer() { | |
| const input = document.getElementById('quiz-input'); | |
| const answer = parseFloat(input.value); | |
| const problem = state.quiz.questions[state.quiz.index]; | |
| if (isNaN(answer)) return; | |
| const correct = answer === problem.answer; | |
| const fb = document.getElementById('quiz-feedback'); | |
| if (correct) { | |
| state.quiz.score++; | |
| fb.className = 'feedback-correct'; | |
| fb.textContent = 'โ Correct!'; | |
| ProgressStore.recordAnswer(problem.type, true); | |
| } else { | |
| fb.className = 'feedback-incorrect'; | |
| const display = problem.answerDisplay || problem.answer; | |
| fb.textContent = `โ Answer: ${display}`; | |
| ProgressStore.recordAnswer(problem.type, false); | |
| } | |
| updateHeader(); | |
| input.disabled = true; | |
| document.getElementById('quiz-submit').disabled = true; | |
| setTimeout(function() { | |
| if (!state.quiz.active) return; | |
| state.quiz.index++; | |
| if (state.quiz.index >= state.quiz.questions.length) { | |
| clearInterval(state.quiz.timer); | |
| endQuiz(); | |
| } else { | |
| showQuizProblem(); | |
| } | |
| }, 800); | |
| } | |
| function endQuiz() { | |
| state.quiz.active = false; | |
| const total = state.quiz.questions.length; | |
| const score = state.quiz.score; | |
| const pct = Math.round((score / total) * 100); | |
| const data = ProgressStore.recordQuiz(state.selectedTopic, score, total, state.selectedDifficulty); | |
| updateHeader(); | |
| let stars = 'โญ'; | |
| if (pct >= 100) stars = 'โญโญโญ'; | |
| else if (pct >= 80) stars = 'โญโญ'; | |
| document.getElementById('results-stars').textContent = stars; | |
| document.getElementById('results-correct').textContent = score; | |
| document.getElementById('results-total').textContent = total; | |
| document.getElementById('results-pct').textContent = pct + '%'; | |
| if (pct >= 100) document.getElementById('results-title').textContent = '๐ Perfect Score!'; | |
| else if (pct >= 80) document.getElementById('results-title').textContent = 'Great Job! ๐'; | |
| else if (pct >= 60) document.getElementById('results-title').textContent = 'Good Effort! ๐ช'; | |
| else document.getElementById('results-title').textContent = 'Keep Practicing! ๐'; | |
| const pointsEarned = pct >= 80 ? 30 : pct >= 60 ? 15 : 5; | |
| document.getElementById('results-points').textContent = pointsEarned; | |
| document.getElementById('results-streak').textContent = data.streak; | |
| navigate('results'); | |
| } | |
| function renderProgress() { | |
| const data = ProgressStore.load(); | |
| const attempted = data.totalAttempted || 1; | |
| document.getElementById('stat-points').textContent = data.points; | |
| document.getElementById('stat-streak').textContent = data.bestStreak; | |
| document.getElementById('stat-accuracy').textContent = Math.round((data.totalCorrect / attempted) * 100) + '%'; | |
| const topics = ['addition', 'subtraction', 'multiplication', 'division', 'word-problems']; | |
| const labels = { 'addition': 'โ Add', 'subtraction': 'โ Sub', 'multiplication': 'โ๏ธ Mul', 'division': 'โ Div', 'word-problems': '๐ Word' }; | |
| const breakdown = document.getElementById('topic-breakdown'); | |
| breakdown.innerHTML = ''; | |
| topics.forEach(t => { | |
| const stats = ProgressStore.getTopicStats(t); | |
| const pct = stats.attempted > 0 ? Math.round((stats.correct / stats.attempted) * 100) : 0; | |
| const row = document.createElement('div'); | |
| row.className = 'topic-row'; | |
| row.innerHTML = ` | |
| <span class="topic-name">${labels[t] || t}</span> | |
| <div class="topic-bar-bg"><div class="topic-bar-fill" style="width:${pct}%"></div></div> | |
| <span class="topic-pct">${pct}%</span> | |
| `; | |
| breakdown.appendChild(row); | |
| }); | |
| const hist = document.getElementById('quiz-history'); | |
| hist.innerHTML = ''; | |
| if (data.quizHistory.length === 0) { | |
| hist.innerHTML = '<p style="color:#999;text-align:center">No quizzes yet. Take one to see it here!</p>'; | |
| } else { | |
| const recent = [...data.quizHistory].reverse().slice(0, 10); | |
| recent.forEach(h => { | |
| const div = document.createElement('div'); | |
| div.className = 'quiz-history-item'; | |
| const date = new Date(h.date); | |
| const dateStr = date.toLocaleDateString(); | |
| const pct = Math.round((h.score / h.total) * 100); | |
| div.innerHTML = ` | |
| <span>${h.topic} (Lv${h.difficulty})</span> | |
| <span>${h.score}/${h.total} (${pct}%) - ${dateStr}</span> | |
| `; | |
| hist.appendChild(div); | |
| }); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| navigate('home'); | |
| }); | |