const termName = (c.term?.name || '').toLowerCase(); if (termName.includes('archive')) return false; const termEnd = c.term?.end_at ? new Date(c.term.end_at) : null; const termStart = c.term?.start_at ? new Date(c.term.start_at) : null; if (termEnd && termEnd < new Date(now.getFullYear(), 0, 1)) return false; if (termStart && termStart > new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)) return false; return true; }); // Get assignments for each active course let added = 0; for (const course of activeCourses.slice(0, 8)) { try { const assignments = await canvasFetch(`/api/v1/courses/${course.id}/assignments?per_page=50&include[]=submission&order_by=due_at`); if (!Array.isArray(assignments)) continue; for (const a of assignments) { if (!a.name || !a.points_possible) continue; const existing = TR.assignments.find(t => t.canvasId === a.id); if (existing) continue; // already in tracker const submission = a.submission; const isLate = submission?.late || false; const isDone = submission?.workflow_state === 'graded' || submission?.workflow_state === 'submitted'; const isMissing = submission?.missing || (!submission?.submitted_at && a.due_at && new Date(a.due_at) < now); const assignment = { id: Date.now() + Math.random(), canvasId: a.id, title: a.name, due: a.due_at, priority: isMissing ? 'high' : 'medium', text: a.description ? a.description.replace(/<[^>]*>/g, '').slice(0, 500) : '', notes: '', subject: detectSubjectFromCourse(course.name), status: isDone ? 'done' : isLate || isMissing ? 'late' : 'todo', points: a.points_possible, courseId: course.id, courseName: course.name, created: new Date().toISOString(), }; TR.assignments.push(assignment); added++; } } catch(e) { console.log('Error fetching assignments for', course.name, e); } } trackerSave(); if (btn) { btn.textContent = '↻ Sync Canvas'; btn.disabled = false; } toast(`✓ Synced ${added} new assignments from Canvas`); } catch(e) { if (btn) { btn.textContent = '↻ Sync Canvas'; btn.disabled = false; } toast('Error syncing: ' + e.message); } } function detectSubjectFromCourse(name) { const n = name.toLowerCase(); if (n.includes('math') || n.includes('algebra') || n.includes('calculus') || n.includes('geometry') || n.includes('amdm') || n.includes('statistics')) return 'math'; if (n.includes('physics') || n.includes('chemistry') || n.includes('biology') || n.includes('science') || n.includes('research')) return 'science'; if (n.includes('history') || n.includes('social') || n.includes('us ')) return 'history'; if (n.includes('english') || n.includes('lit') || n.includes('writing') || n.includes('comp') || n.includes('journalism') || n.includes('newspaper')) return 'english'; if (n.includes('computer') || n.includes('coding') || n.includes('program') || n.includes('cs') || n.includes('csp') || n.includes('apps') || n.includes('game')) return 'cs'; if (n.includes('art') || n.includes('film') || n.includes('video') || n.includes('media') || n.includes('design') || n.includes('animation')) return 'art'; if (n.includes('business') || n.includes('economics')) return 'business'; return 'other'; } function trackerSave() { localStorage.setItem(TRACKER_KEY, JSON.stringify(TR.assignments)); trackerRender(); trackerUpdateBadge(); } function trackerUpdateBadge() { const badge = document.getElementById('tracker-badge'); const active = TR.assignments.filter(a => a.status !== 'done').length; if (active > 0) { badge.textContent = active; badge.style.display = 'inline'; } else badge.style.display = 'none'; } function trackerOpenAdd() { document.getElementById('tracker-modal').style.display = 'flex'; document.getElementById('ta-title').focus(); } function trackerCloseAdd() { document.getElementById('tracker-modal').style.display = 'none'; ['ta-title','ta-text','ta-notes','ta-due'].forEach(id => document.getElementById(id).value = ''); document.getElementById('ta-priority').value = 'medium'; } async function trackerSaveAdd() { const title = document.getElementById('ta-title').value.trim(); const due = document.getElementById('ta-due').value; const priority = document.getElementById('ta-priority').value; const text = document.getElementById('ta-text').value.trim(); const notes = document.getElementById('ta-notes').value.trim(); if (!title) { toast('⚠ Enter an assignment title'); return; } const btn = document.querySelector('#tracker-modal .btn.primary'); btn.textContent = '✦ Detecting…'; btn.disabled = true; let subject = 'other'; if (text && S.claudeKey) { try { const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type':'application/json','x-api-key':S.claudeKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true' }, body: JSON.stringify({ model: CLAUDE_MODEL, max_tokens: 20, system: 'Classify the academic subject of this assignment. Reply with ONLY one word from: math, science, biology, chemistry, physics, history, english, writing, cs, business, economics, art, other', messages: [{ role:'user', content: text.slice(0,1000) }], }), }); const data = await res.json(); const raw = (data?.content?.[0]?.text || 'other').trim().toLowerCase().replace(/[^a-z]/g,''); subject = SUBJECT_MAP[raw] ? raw : 'other'; } catch(e) { subject = 'other'; } } const assignment = { id: Date.now(), title, due, priority, text, notes, subject, status: 'todo', created: new Date().toISOString(), }; TR.assignments.push(assignment); trackerSave(); trackerCloseAdd(); btn.textContent = '✦ Add & Detect Subject'; btn.disabled = false; toast(`✓ Added "${title}" · Subject: ${SUBJECT_MAP[subject].label}`); } function trackerDelete(id) { TR.assignments = TR.assignments.filter(a => a.id !== id); trackerSave(); } function trackerMove(id, status) { const a = TR.assignments.find(a => a.id === id); if (a) { a.status = status; trackerSave(); } } function trackerOpenInAnalyze(id) { const a = TR.assignments.find(a => a.id === id); if (!a || !a.text) { toast('⚠ No assignment text — add text when creating'); return; } // Load as a virtual doc S.activeDoc = { id: 'tracker_' + a.id, source: 'tracker', name: a.title, content: a.text + (a.notes ? '\n\n--- CLASS NOTES ---\n' + a.notes : ''), icon: SUBJECT_MAP[a.subject]?.icon || '📚', meta: SUBJECT_MAP[a.subject]?.label || 'Assignment', }; document.getElementById('active-doc-pill').style.display = 'block'; document.getElementById('active-doc-name').textContent = a.title; setView('analyze'); toast(`✓ Opened "${a.title}" in Analyze`); // Show notes linker if notes present if (a.notes) { document.getElementById('notes-linker-banner').style.display = 'block'; document.getElementById('notes-linker-sub').textContent = `Class notes attached · ${SUBJECT_MAP[a.subject]?.label || 'General'} subject detected`; S._trackerNotesActive = a.notes; } } function trackerUnlinkNotes() { document.getElementById('notes-linker-banner').style.display = 'none'; S._trackerNotesActive = null; if (S.activeDoc) S.activeDoc.content = S.activeDoc.content.split('\n\n--- CLASS NOTES ---\n')[0]; toast('Notes unlinked'); } function trackerUseNotes() { setView('analyze'); setTimeout(() => switchAnalyzeTab('qb'), 100); } // Drag-and-drop let _dragId = null; function trackerDragStart(e, id) { _dragId = id; e.target.classList.add('dragging'); } function trackerDragEnd(e) { e.target.classList.remove('dragging'); } function trackerDrop(e, status) { e.preventDefault(); if (_dragId) { trackerMove(_dragId, status); _dragId = null; } } function trackerRender() { const now2 = new Date(); const cols = { todo:[], late:[], done:[] }; TR.assignments.forEach(a => { // Auto-move to late if past due and not done if (a.status !== 'done' && a.due && new Date(a.due) < now2) { cols.late.push(a); } else { (cols[a.status] || cols.todo).push(a); } }); ['todo','late','done'].forEach(col => { const el = document.getElementById('col-' + col); document.getElementById('count-' + col).textContent = cols[col].length; el.innerHTML = ''; if (!cols[col].length) { el.innerHTML = `
Drop here
`; return; } // Sort todo column if (col === 'todo') { const sortBy = document.getElementById('todo-sort')?.value || 'due'; if (sortBy === 'due') { cols[col].sort((a,b) => { if (!a.due && !b.due) return 0; if (!a.due) return 1; if (!b.due) return -1; return new Date(a.due) - new Date(b.due); }); } else if (sortBy === 'impact') { cols[col].sort((a,b) => (b.gradeImpact||0) - (a.gradeImpact||0)); } } cols[col].forEach(a => { const subj = SUBJECT_MAP[a.subject] || SUBJECT_MAP.other; const dueTxt = a.due ? trackerDueLabel(a.due) : null; const dueCls = a.due ? trackerDueCls(a.due) : 'ok'; const priIcon = { high:'🔴', medium:'🟡', low:'🟢' }[a.priority] || '🟡'; const card = document.createElement('div'); card.className = 'kanban-card'; card.draggable = true; card.addEventListener('dragstart', e => trackerDragStart(e, a.id)); card.addEventListener('dragend', e => trackerDragEnd(e)); card.innerHTML = `
${subj.icon} ${subj.label}
${a.notes ? `
📎 Notes linked
` : ''}
${a.title}
${dueTxt ? `📅 ${dueTxt}` : ''} ${priIcon}
${col !== 'todo' ? `` : ''} ${col === 'late' ? `` : ''} ${col !== 'done' ? `` : ''} ${a.text ? `` : ''}
`; el.appendChild(card); }); }); // Deadline strip const upcoming = TR.assignments .filter(a => a.due && a.status !== 'done') .sort((a,b) => new Date(a.due) - new Date(b.due)) .slice(0, 8); const pills = document.getElementById('deadline-pills'); const empty = document.getElementById('deadline-empty'); pills.innerHTML = ''; if (!upcoming.length) { empty.style.display = 'block'; return; } empty.style.display = 'none'; upcoming.forEach(a => { const cls = trackerDueCls(a.due); const pill = document.createElement('div'); pill.className = `deadline-pill ${cls}`; pill.innerHTML = `${SUBJECT_MAP[a.subject]?.icon || '📚'} ${a.title.slice(0,22)}${a.title.length>22?'…':''} · ${trackerDueLabel(a.due)}`; pill.onclick = () => { setView('tracker'); }; pills.appendChild(pill); }); trackerUpdateBadge(); } function trackerDueLabel(due) { const diff = new Date(due) - new Date(); const h = diff / 3600000; if (h < 0) return 'Overdue'; if (h < 24) return `${Math.round(h)}h left`; const d = Math.floor(h / 24); if (d === 1) return 'Tomorrow'; if (d < 7) return `${d} days`; return new Date(due).toLocaleDateString(undefined,{month:'short',day:'numeric'}); } function trackerDueCls(due) { const h = (new Date(due) - new Date()) / 3600000; if (h < 0) return 'urgent'; if (h < 48) return 'urgent'; if (h < 168) return 'soon'; return 'ok'; } // ═══════════════════════════════════════════════════════════ // ── POMODORO TIMER ────────────────────────────────────────── // ═══════════════════════════════════════════════════════════ const TIMER = { modes: { focus: 25*60, shortBreak: 5*60, longBreak: 15*60 }, mode: 'focus', remaining: 25*60, total: 25*60, running: false, sessions: parseInt(localStorage.getItem('sh_timer_sessions') || '0'), _interval: null, }; function timerToggle() { if (TIMER.running) { clearInterval(TIMER._interval); TIMER.running = false; document.getElementById('timer-start-btn').textContent = 'Resume'; } else { TIMER.running = true; document.getElementById('timer-start-btn').textContent = 'Pause'; TIMER._interval = setInterval(() => { TIMER.remaining--; timerUpdate(); if (TIMER.remaining <= 0) { clearInterval(TIMER._interval); TIMER.running = false; if (TIMER.mode === 'focus') { TIMER.sessions++; localStorage.setItem('sh_timer_sessions', TIMER.sessions); toast('🍅 Focus session complete! Take a break.'); } else { toast('⏱ Break over! Ready to focus?'); } timerUpdate(); document.getElementById('timer-start-btn').textContent = 'Start'; // try notification if (Notification.permission === 'granted') { new Notification('Study Hub', { body: TIMER.mode === 'focus' ? '🍅 Focus session done! Take a break.' : '⏱ Break over — back to work!' }); } } }, 1000); } document.getElementById('timer-start-btn').textContent = TIMER.running ? 'Pause' : 'Resume'; } function timerReset() { clearInterval(TIMER._interval); TIMER.running = false; TIMER.remaining = TIMER.total; document.getElementById('timer-start-btn').textContent = 'Start'; timerUpdate(); } function timerSwitch() { clearInterval(TIMER._interval); TIMER.running = false; const modes = ['focus','shortBreak','longBreak']; const next = modes[(modes.indexOf(TIMER.mode) + 1) % modes.length]; TIMER.mode = next; TIMER.total = TIMER.modes[next]; TIMER.remaining = TIMER.total; const labels = { focus:'FOCUS', shortBreak:'5 MIN', longBreak:'15 MIN' }; document.getElementById('timer-mode-pill').textContent = labels[next]; document.getElementById('timer-switch-btn').textContent = next === 'focus' ? 'Break' : next === 'shortBreak' ? 'Long' : 'Focus'; document.getElementById('timer-start-btn').textContent = 'Start'; timerUpdate(); // Request notification permission if (Notification.permission === 'default') Notification.requestPermission(); } function timerUpdate() { const m = Math.floor(TIMER.remaining / 60).toString().padStart(2,'0'); const s = (TIMER.remaining % 60).toString().padStart(2,'0'); document.getElementById('timer-display').textContent = `${m}:${s}`; const pct = (TIMER.remaining / TIMER.total) * 100; document.getElementById('timer-bar').style.width = pct + '%'; const col = TIMER.mode === 'focus' ? 'var(--green)' : 'var(--blue)'; document.getElementById('timer-bar').style.background = `linear-gradient(90deg,${col},var(--blue))`; document.getElementById('timer-sessions').textContent = `🍅 ${TIMER.sessions} session${TIMER.sessions!==1?'s':''} today`; // change display color when low const display = document.getElementById('timer-display'); display.style.color = TIMER.remaining < 60 && TIMER.mode === 'focus' ? 'var(--red)' : 'var(--text)'; } // ═══════════════════════════════════════════════════════════ // ── GRADE ANALYZER — Canvas API direct ────────────────────── // ═══════════════════════════════════════════════════════════ let GR = { courseId: null, courseName: '' }; function gradesInitView() { if (S.canvasToken && S.canvasUrl) { document.getElementById('grades-no-token').style.display = 'none'; document.getElementById('grades-connected').style.display = 'flex'; document.getElementById('grades-canvas-domain').textContent = S.canvasUrl; gradesLoadCourses(); } else { document.getElementById('grades-no-token').style.display = 'block'; document.getElementById('grades-connected').style.display = 'none'; } } function quickSaveCanvasToken() { const url = document.getElementById('quick-canvas-url').value.trim().replace(/^https?:\/\//, '').replace(/\/$/, ''); const token = document.getElementById('quick-canvas-token').value.trim(); if (!url || !token) { toast('⚠ Enter both your Canvas URL and token'); return; } S.canvasUrl = url; S.canvasToken = token; localStorage.setItem('sh_canvas_url', url); localStorage.setItem('sh_canvas_token', token); // sync to settings inputs if (document.getElementById('s-canvas-url')) document.getElementById('s-canvas-url').value = url; if (document.getElementById('s-canvas-token')) document.getElementById('s-canvas-token').value = token; toast('✓ Canvas connected!'); gradesInitView(); } function gradesDisconnect() { S.canvasToken = ''; S.canvasUrl = ''; localStorage.removeItem('sh_canvas_token'); localStorage.removeItem('sh_canvas_url'); gradesInitView(); } async function canvasFetch(path) { if (!S.n8nUrl) throw new Error('Add your n8n URL in Settings'); const res = await fetch(`${S.n8nUrl}/webhook/canvas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ canvasUrl: S.canvasUrl, token: S.canvasToken, path }), }); if (!res.ok) throw new Error(`Proxy error ${res.status}`); const text = await res.text(); if (!text || text.length < 2) throw new Error('Empty response from proxy'); const json = JSON.parse(text); // Handle all possible response shapes if (Array.isArray(json)) return json; if (json && Array.isArray(json.courses)) return json.courses; if (json && Array.isArray(json.result)) return json.result; if (json && Array.isArray(json.data)) return json.data; if (json && typeof json === 'object') return [json]; return json; } async function gradesLoadCourses() { const list = document.getElementById('grades-course-list'); list.innerHTML = `
Loading courses…
`; try { // Fetch active enrollments let allCourses = await canvasFetch('/api/v1/courses?per_page=100&include[]=term&enrollment_type=student'); if (!Array.isArray(allCourses)) allCourses = []; const now = new Date(); let courses = allCourses.filter(c => { if (!c.name || c.access_restricted_by_date) return false; if (c.workflow_state !== 'available') return false; if (c.term) { if (c.term.name && c.term.name.toLowerCase().includes('archive')) return false; const termEnd = c.term.end_at ? new Date(c.term.end_at) : null; if (termEnd && termEnd < new Date(now.getFullYear() - 1, 0, 1)) return false; } return true; }); list.innerHTML = ''; if (!courses || !courses.length) { list.innerHTML = `
No active courses found.
`; return; } courses .filter(c => c.name) .forEach(c => { const card = document.createElement('div'); card.style.cssText = 'padding:12px 16px;border-radius:11px;border:1px solid var(--border);background:rgba(255,255,255,0.02);cursor:pointer;transition:all 0.15s;display:flex;align-items:center;gap:12px;'; card.innerHTML = `
🎓
${c.name}
${c.course_code ? `
${c.course_code}
` : ''}
`; card.onmouseenter = () => { card.style.background='rgba(255,255,255,0.06)'; card.style.borderColor='var(--border-hi)'; }; card.onmouseleave = () => { card.style.background='rgba(255,255,255,0.02)'; card.style.borderColor='var(--border)'; }; card.onclick = () => gradesLoadCourse(c.id, c.name); list.appendChild(card); }); } catch(e) { list.innerHTML = `
Error: ${e.message}
Check your Canvas URL and token in Settings.
`; } } async function gradesLoadCourse(courseId, courseName) { GR.courseId = courseId; GR.courseName = courseName; document.getElementById('grades-active-course-name').textContent = courseName; document.getElementById('grades-paste-panel').style.display = 'none'; document.getElementById('grades-results-panel').style.display = 'flex'; document.getElementById('grades-clear-btn').style.display = 'inline-flex'; document.getElementById('grades-analyze-btn').style.display = 'none'; // Show loading state const sumBar = document.getElementById('grades-summary-bar'); const scenarios = document.getElementById('grades-scenarios'); const priList = document.getElementById('grades-priority-list'); const allList = document.getElementById('grades-all-list'); sumBar.innerHTML = `
Fetching grades from Canvas…
`; scenarios.innerHTML = ''; priList.innerHTML = ''; allList.innerHTML = ''; try { // Fetch assignment groups (gives us weights) + assignments in parallel const [groups, submissions] = await Promise.all([ canvasFetch(`/api/v1/courses/${courseId}/assignment_groups?include[]=assignments&include[]=submission`), canvasFetch(`/api/v1/courses/${courseId}/students/submissions?student_ids[]=self&per_page=100&include[]=assignment`), ]); // Build a submission map: assignment_id → submission const subMap = {}; submissions.forEach(s => { subMap[s.assignment_id] = s; }); // Build normalised assignment list const assignments = []; groups.forEach(group => { (group.assignments || []).forEach(a => { if (!a.published) return; if (a.submission_types?.includes('not_graded')) return; const sub = subMap[a.id]; const earned = sub?.score ?? null; // null = not submitted / missing const missing = sub?.missing || sub?.workflow_state === 'unsubmitted' || earned === null; const possible = a.points_possible || 0; if (!possible) return; // skip 0-point assignments assignments.push({ name: a.name, earned: missing ? null : earned, possible: possible, weight: group.group_weight || null, group: group.name, due: a.due_at, }); }); }); if (!assignments.length) { sumBar.innerHTML = `
No graded assignments found for this course yet.
`; return; } gradesRender(assignments); } catch(e) { sumBar.innerHTML = `
Error loading grades: ${e.message}
`; } } async function gradesRefresh() { if (GR.courseId) gradesLoadCourse(GR.courseId, GR.courseName); } function gradesBackToCourses() { GR.courseId = null; document.getElementById('grades-paste-panel').style.display = 'flex'; document.getElementById('grades-results-panel').style.display = 'none'; document.getElementById('grades-clear-btn').style.display = 'none'; } function gradesClear() { gradesBackToCourses(); } // ── GRADE MATH & RENDERING ─────────────────────────────────── function letterGrade(pct) { if (pct >= 93) return 'A'; if (pct >= 90) return 'A-'; if (pct >= 87) return 'B+'; if (pct >= 83) return 'B'; if (pct >= 80) return 'B-'; if (pct >= 77) return 'C+'; if (pct >= 73) return 'C'; if (pct >= 70) return 'C-'; if (pct >= 67) return 'D+'; if (pct >= 60) return 'D'; return 'F'; } function gradeColor(pct) { if (pct >= 90) return 'var(--green)'; if (pct >= 80) return 'var(--blue)'; if (pct >= 70) return 'var(--amber)'; return 'var(--red)'; } function calcGrade(items, hasWeights) { if (!items.length) return 0; if (hasWeights) { let totalWeight = 0, earned = 0; items.forEach(a => { const w = a.weight || 0; const pct = a.earned !== null ? (a.earned / a.possible) * 100 : 0; earned += pct * w; totalWeight += w; }); return totalWeight ? earned / totalWeight : 0; } else { const e = items.reduce((s, a) => s + (a.earned !== null ? a.earned : 0), 0); const p = items.reduce((s, a) => s + a.possible, 0); return p ? (e / p) * 100 : 0; } } function gradesRender(assignments) { const hasWeights = assignments.some(a => a.weight !== null && a.weight > 0); const zeros = assignments.filter(a => a.earned === null || a.earned === 0); const scored = assignments.filter(a => a.earned !== null && a.earned > 0); const allWithZeros = [...scored, ...zeros.map(a => ({ ...a, earned: 0 }))]; const gradeNow = calcGrade(allWithZeros, hasWeights); const grade70 = calcGrade([...scored, ...zeros.map(a => ({ ...a, earned: a.possible * 0.70 }))], hasWeights); const grade100 = calcGrade([...scored, ...zeros.map(a => ({ ...a, earned: a.possible }))], hasWeights); // Per-zero impact: what does grade become if just this one becomes 100? const zeroImpacts = zeros.map(a => { const with100 = calcGrade([ ...scored, ...zeros.map(z => z === a ? { ...z, earned: z.possible } : { ...z, earned: 0 }), ], hasWeights); const with70 = calcGrade([ ...scored, ...zeros.map(z => z === a ? { ...z, earned: z.possible * 0.7 } : { ...z, earned: 0 }), ], hasWeights); return { ...a, impact100: +(with100 - gradeNow).toFixed(2), impact70: +(with70 - gradeNow).toFixed(2) }; }).sort((a, b) => b.impact100 - a.impact100); // ── Summary bar ──────────────────────────────────────────── document.getElementById('grades-summary-bar').innerHTML = `
${gradeNow.toFixed(1)}% ${letterGrade(gradeNow)}
Current Grade
${zeros.length}
Missing / Zeros
${scored.length}
Graded
${assignments.length}
Total
${hasWeights ? `
Weighted grading
` : ''} `; // ── Scenario cards ───────────────────────────────────────── function scenarioCard(label, grade, note) { const delta = grade - gradeNow; const sign = delta >= 0 ? '+' : ''; const col = gradeColor(grade); return `
${label}
${grade.toFixed(1)}% ${letterGrade(grade)}
${sign}${delta.toFixed(1)}% vs now
${note}
`; } document.getElementById('grades-scenarios').innerHTML = scenarioCard('Current Grade', gradeNow, 'As it stands right now') + scenarioCard('If All Zeros → 70%', grade70, `${zeros.length} missing at minimum pass`) + scenarioCard('If All Zeros → 100%', grade100, `${zeros.length} missing at perfect score`); // ── Priority list ───────────────────────────────────────── const priList = document.getElementById('grades-priority-list'); priList.innerHTML = ''; if (!zeroImpacts.length) { priList.innerHTML = `
🎉 No missing assignments! All zeros cleared.
`; } else { zeroImpacts.forEach((a, i) => { const rankCls = i === 0 ? 'p1' : i === 1 ? 'p2' : i === 2 ? 'p3' : 'pn'; const urgency = i === 0 ? '🔴 Highest impact' : i === 1 ? '🟡 High impact' : i < 5 ? '🟢 Medium impact' : '⚪ Lower impact'; const dueStr = a.due ? `· Due ${new Date(a.due).toLocaleDateString(undefined,{month:'short',day:'numeric'})}` : ''; const row = document.createElement('div'); row.className = 'priority-row'; row.innerHTML = `
${i + 1}
${a.name}
${urgency} · ${a.possible} pts${a.weight ? ` · ${a.weight}% weight` : ''} ${dueStr}${a.group ? ` · ${a.group}` : ''}
+${a.impact100.toFixed(1)}%
if 100%
+${a.impact70.toFixed(1)}% if 70%
`; priList.appendChild(row); }); } // ── All assignments ──────────────────────────────────────── const allList = document.getElementById('grades-all-list'); allList.innerHTML = ''; assignments.forEach(a => { const pct = a.earned !== null ? (a.earned / a.possible) * 100 : 0; const missing = a.earned === null; const zero = a.earned === 0; const col = (missing || zero) ? 'var(--red)' : pct >= 90 ? 'var(--green)' : pct >= 70 ? 'var(--amber)' : 'var(--red)'; const scoreText = missing ? `Missing` : zero ? `0 / ${a.possible}` : `${a.earned} / ${a.possible} (${pct.toFixed(0)}%)`; const dueStr = a.due ? `${new Date(a.due).toLocaleDateString(undefined,{month:'short',day:'numeric'})}` : ''; allList.innerHTML += `
${(missing||zero)?'⚠ ':''}${a.name} ${dueStr}
${a.group ? `${a.group}` : ''}
${scoreText}
${a.weight ? `
${a.weight}%
` : ''}
`; }); } // ═══════════════════════════════════════════════════════════ // ── QUIZ PREP ─────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════ const QZ = { questions: [], // parsed question objects current: 0, score: 0, answered: 0, taskHistory: [], // for procrastination breaker context }; async function quizGenerate() { if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); return; } const topic = document.getElementById('quiz-topic').value.trim(); const notes = document.getElementById('quiz-notes').value.trim(); const type = document.getElementById('quiz-type').value; const diff = document.getElementById('quiz-diff').value; const count = document.getElementById('quiz-count').value; if (!topic && !notes) { toast('⚠ Enter a topic or paste some notes'); return; } const btn = document.getElementById('quiz-gen-btn2'); btn.textContent = 'Generating…'; btn.disabled = true; const typeDesc = { mixed: 'a mix of multiple-choice and short-answer', mcq: 'multiple-choice only (4 options each)', short: 'short-answer only', truefalse: 'true/false only', fillblank: 'fill-in-the-blank only' }[type]; const diffDesc = { easy: 'basic recall and definitions', medium: 'conceptual understanding', hard: 'application and analysis', exam: 'mixed difficulty mimicking a real exam' }[diff]; const prompt = `Generate exactly ${count} quiz questions about: ${topic || 'the material below'}. Format: ${typeDesc}. Difficulty: ${diffDesc}. ${notes ? `\nBase questions on this material:\n${notes.slice(0, 6000)}` : ''} Return ONLY valid JSON — no markdown fences, no explanation: [ { "type": "mcq" | "short" | "truefalse" | "fillblank", "question": "question text", "options": ["A) ...", "B) ...", "C) ...", "D) ..."], // only for mcq "correct": "A" | "B" | "C" | "D" | "True" | "False" | "exact answer string", "explanation": "brief explanation of the correct answer" } ] For short/fillblank, omit options. correct = the ideal answer string.`; try { const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type':'application/json','x-api-key':S.claudeKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true' }, body: JSON.stringify({ model: CLAUDE_MODEL, max_tokens: 3000, messages: [{ role:'user', content: prompt }] }), }); const data = await res.json(); const raw = (data?.content?.[0]?.text || '[]').replace(/```json|```/g,'').trim(); QZ.questions = JSON.parse(raw); QZ.current = 0; QZ.score = 0; QZ.answered = 0; } catch(e) { toast('Error generating quiz: ' + e.message); btn.textContent = '✦ Generate Quiz'; btn.disabled = false; return; } btn.textContent = '✦ Generate Quiz'; btn.disabled = false; if (!QZ.questions.length) { toast('⚠ No questions generated — try being more specific'); return; } document.getElementById('quiz-setup').style.display = 'none'; document.getElementById('quiz-active').style.display = 'flex'; document.getElementById('quiz-reset-btn').style.display = 'inline-flex'; document.getElementById('quiz-gen-btn').style.display = 'none'; quizShowQuestion(); } function quizShowQuestion() { const q = QZ.questions[QZ.current]; const tot = QZ.questions.length; const pct = ((QZ.current) / tot) * 100; document.getElementById('quiz-progress-label').textContent = `Q${QZ.current + 1} of ${tot}`; document.getElementById('quiz-progress-bar').style.width = pct + '%'; document.getElementById('quiz-score-display').textContent = `Score: ${QZ.score}/${QZ.answered}`; document.getElementById('quiz-results').style.display = 'none'; const area = document.getElementById('quiz-question-area'); area.style.display = 'flex'; area.innerHTML = ''; const card = document.createElement('div'); card.className = 'quiz-question-card'; if (q.type === 'mcq') { const letters = ['A','B','C','D']; card.innerHTML = `
Question ${QZ.current + 1} · Multiple Choice
${q.question}
${(q.options || []).map((opt, i) => `
${letters[i]}
${opt.replace(/^[A-D]\)\s*/,'')}
`).join('')} `; } else if (q.type === 'truefalse') { card.innerHTML = `
Question ${QZ.current + 1} · True / False
${q.question}
✓ True
✗ False
`; } else { // short answer / fill-in-blank card.innerHTML = `
Question ${QZ.current + 1} · ${q.type === 'fillblank' ? 'Fill in the Blank' : 'Short Answer'}
${q.question}
`; } area.appendChild(card); area.scrollTop = 0; } function quizAnswerMCQ(selected, idx) { const q = QZ.questions[QZ.current]; const correct = (selected === q.correct) || (selected.replace(/^[A-D]\)\s*/,'').trim() === q.correct.trim()); document.querySelectorAll('[id^="qopt-"]').forEach(el => el.onclick = null); const letters = ['A','B','C','D','True','False']; const correctIdx = letters.indexOf(q.correct); const opts = document.querySelectorAll('[id^="qopt-"]'); opts.forEach((el, i) => { if (i === correctIdx || letters[i] === q.correct) el.classList.add('correct'); else if (i === idx && !correct) el.classList.add('wrong'); }); if (correct) QZ.score++; QZ.answered++; quizShowFeedback(correct, q.explanation); } async function quizAnswerShort() { const q = QZ.questions[QZ.current]; const ans = document.getElementById('quiz-short-ans').value.trim(); if (!ans) return; document.getElementById('quiz-short-ans').disabled = true; const fb = document.getElementById('quiz-feedback'); fb.style.display = 'block'; fb.className = 'quiz-feedback'; fb.innerHTML = `
Grading…
`; try { const res = await fetch('https://api.anthropic.com/v1/messages', { method:'POST', headers:{'Content-Type':'application/json','x-api-key':S.claudeKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'}, body: JSON.stringify({ model: CLAUDE_MODEL, max_tokens: 200, messages:[{ role:'user', content:`Question: ${q.question}\nIdeal answer: ${q.correct}\nStudent's answer: ${ans}\n\nIs the student's answer correct or substantially correct? Reply with ONLY: CORRECT or INCORRECT, then one sentence of feedback.` }], }), }); const data = await res.json(); const result = data?.content?.[0]?.text || 'INCORRECT'; const isCorrect = result.trim().toUpperCase().startsWith('CORRECT'); const feedback = result.replace(/^(CORRECT|INCORRECT)[.:]?\s*/i,''); if (isCorrect) QZ.score++; QZ.answered++; fb.className = 'quiz-feedback ' + (isCorrect ? 'correct' : 'wrong'); fb.innerHTML = `${isCorrect ? '✓ Correct!' : '✗ Not quite'}
${feedback}
Model answer: ${q.correct}`; } catch(e) { fb.innerHTML = `Correct answer: ${q.correct}
${q.explanation || ''}`; } document.getElementById('quiz-next-btn').style.display = 'inline-block'; document.getElementById('quiz-score-display').textContent = `Score: ${QZ.score}/${QZ.answered}`; } function quizRevealShort() { const q = QZ.questions[QZ.current]; const fb = document.getElementById('quiz-feedback'); fb.style.display = 'block'; fb.className = 'quiz-feedback wrong'; fb.innerHTML = `Answer: ${q.correct}
${q.explanation || ''}`; QZ.answered++; document.getElementById('quiz-next-btn').style.display = 'inline-block'; document.getElementById('quiz-score-display').textContent = `Score: ${QZ.score}/${QZ.answered}`; } function quizShowFeedback(correct, explanation) { const fb = document.getElementById('quiz-feedback'); fb.style.display = 'block'; fb.className = 'quiz-feedback ' + (correct ? 'correct' : 'wrong'); fb.innerHTML = `${correct ? '✓ Correct!' : '✗ Incorrect'} ${explanation || ''}`; document.getElementById('quiz-next-btn').style.display = 'inline-block'; document.getElementById('quiz-score-display').textContent = `Score: ${QZ.score}/${QZ.answered}`; } function quizNext() { QZ.current++; if (QZ.current >= QZ.questions.length) { quizShowResults(); return; } quizShowQuestion(); } function quizShowResults() { const pct = Math.round((QZ.score / QZ.questions.length) * 100); const col = pct >= 80 ? 'var(--green)' : pct >= 60 ? 'var(--amber)' : 'var(--red)'; const msg = pct >= 90 ? '🎉 Excellent work!' : pct >= 70 ? '👍 Good effort — review what you missed.' : '📖 Keep studying — you\'ve got this.'; document.getElementById('quiz-progress-bar').style.width = '100%'; document.getElementById('quiz-question-area').style.display = 'none'; const results = document.getElementById('quiz-results'); results.style.display = 'flex'; results.innerHTML = `
${pct}%
${QZ.score} / ${QZ.questions.length} correct
${msg}
`; } function quizReset() { QZ.questions = []; QZ.current = 0; QZ.score = 0; QZ.answered = 0; document.getElementById('quiz-setup').style.display = 'flex'; document.getElementById('quiz-active').style.display = 'none'; document.getElementById('quiz-reset-btn').style.display = 'none'; document.getElementById('quiz-gen-btn').style.display = 'none'; } function quizRetry() { QZ.current = 0; QZ.score = 0; QZ.answered = 0; document.getElementById('quiz-results').style.display = 'none'; document.getElementById('quiz-question-area').style.display = 'flex'; quizShowQuestion(); } // ═══════════════════════════════════════════════════════════ // ── CITATION MANAGER ──────────────────────────────────────── // ═══════════════════════════════════════════════════════════ const CIT = { sources: JSON.parse(localStorage.getItem('sh_citations') || '[]') }; function citationSave() { localStorage.setItem('sh_citations', JSON.stringify(CIT.sources)); } function citationRender() { const list = document.getElementById('cit-list'); const empty = document.getElementById('cit-empty'); const copyAll = document.getElementById('cit-copy-all-btn'); if (!CIT.sources.length) { empty.style.display = 'flex'; copyAll.style.display = 'none'; list.innerHTML = ''; list.appendChild(empty); return; } empty.style.display = 'none'; copyAll.style.display = 'inline-flex'; list.innerHTML = ''; CIT.sources.forEach((src, i) => { const card = document.createElement('div'); card.className = 'citation-card'; card.innerHTML = `
${src.format} Source ${i + 1}
${src.formatted}
${src.quote ? `
"${src.quote}"
` : ''} ${src.summary ? `
${src.summary}
` : ''}
${src.quote ? `` : ''}
`; list.appendChild(card); }); } async function citationAdd() { if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); return; } const input = document.getElementById('cit-input').value.trim(); const format = document.getElementById('cit-format').value; if (!input) return; const btn = document.getElementById('cit-add-btn'); btn.textContent = 'Formatting…'; btn.disabled = true; const prompt = `You are a citation and source assistant. The user has provided this source reference: "${input}" Do the following: 1. Format a complete ${format} citation for this source. If it's a URL, infer what you can from the URL and use "[Retrieved from URL]" for what you can't know. If it's a DOI, format accordingly. 2. Extract or suggest one relevant short quote (1-2 sentences) that might be useful for academic writing. If you can't access the source, suggest a placeholder. 3. Write a 1-sentence summary of what this source is likely about. Respond ONLY with valid JSON, no markdown: { "formatted": "full formatted citation in ${format}", "quote": "a short useful quote or empty string", "summary": "one sentence about this source" }`; try { const res = await fetch('https://api.anthropic.com/v1/messages', { method:'POST', headers:{'Content-Type':'application/json','x-api-key':S.claudeKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'}, body: JSON.stringify({ model: CLAUDE_MODEL, max_tokens: 500, messages:[{ role:'user', content: prompt }] }), }); const data = await res.json(); const raw = (data?.content?.[0]?.text || '{}').replace(/```json|```/g,'').trim(); const parsed = JSON.parse(raw); CIT.sources.unshift({ ...parsed, format, input, added: new Date().toISOString() }); citationSave(); document.getElementById('cit-input').value = ''; citationRender(); toast('✓ Citation added'); } catch(e) { toast('Error: ' + e.message); } btn.textContent = 'Format →'; btn.disabled = false; } function citationDelete(i) { CIT.sources.splice(i, 1); citationSave(); citationRender(); } function citationCopyAll() { const all = CIT.sources.map((s, i) => `${i + 1}. ${s.formatted}`).join('\n\n'); navigator.clipboard.writeText(all); toast(`✓ ${CIT.sources.length} citations copied`); } function citationSendToNotes(i) { const s = CIT.sources[i]; const ta = document.getElementById('notes-textarea'); ta.value += `\n\n── Source ──\n${s.formatted}${s.quote ? '\n"' + s.quote + '"' : ''}`; S.notes = ta.value; localStorage.setItem('sh_notes', S.notes); toast('✓ Saved to Notes'); } // ═══════════════════════════════════════════════════════════ // ── PROCRASTINATION BREAKER / FOCUS MODE ──────────────────── // ═══════════════════════════════════════════════════════════ const FC = { steps: [], current: 0, task: '', timerSecs: 120, timerRunning: false, _interval: null, }; async function focusBreak() { if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); return; } const task = document.getElementById('focus-task-input').value.trim(); if (!task) { toast('⚠ Describe the task you\'re avoiding'); return; } FC.task = task; FC.steps = []; FC.current = 0; const btn = document.querySelector('#focus-setup button:last-child'); btn.textContent = 'Breaking it down…'; btn.disabled = true; const step = await focusFetchStep(task, []); btn.textContent = '🎯 Give me my first step'; btn.disabled = false; if (!step) return; FC.steps.push(step); document.getElementById('focus-setup').style.display = 'none'; document.getElementById('focus-session').style.display = 'flex'; focusRenderStep(); } async function focusFetchStep(task, doneSteps) { const doneCtx = doneSteps.length ? `\nCompleted steps so far:\n${doneSteps.map((s,i) => `${i+1}. ${s.action}`).join('\n')}` : ''; const prompt = `A student is procrastinating on this task: "${task}"${doneCtx} Give them their ${doneSteps.length === 0 ? 'first' : 'next'} micro-step — something so small and specific it takes under 2 minutes to start. Respond ONLY with JSON: { "action": "The single specific micro-step (one sentence, starts with a verb)", "why": "One sentence on why this tiny step helps break the block", "minutes": 2 }`; try { const res = await fetch('https://api.anthropic.com/v1/messages', { method:'POST', headers:{'Content-Type':'application/json','x-api-key':S.claudeKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'}, body: JSON.stringify({ model: CLAUDE_MODEL, max_tokens: 200, messages:[{ role:'user', content: prompt }] }), }); const data = await res.json(); const raw = (data?.content?.[0]?.text || '{}').replace(/```json|```/g,'').trim(); return JSON.parse(raw); } catch(e) { toast('Error: ' + e.message); return null; } } function focusRenderStep() { const step = FC.steps[FC.current]; document.getElementById('focus-step-text').textContent = step.action; document.getElementById('focus-step-why').textContent = step.why || ''; document.getElementById('focus-time-est').textContent = `⏱ ~${step.minutes || 2} minutes`; // Reset timer clearInterval(FC._interval); FC.timerRunning = false; FC.timerSecs = (step.minutes || 2) * 60; focusTimerUpdate(); document.getElementById('focus-timer-btn').textContent = `Start ${step.minutes || 2}-min timer`; // Render history const hist = document.getElementById('focus-step-history'); hist.innerHTML = ''; FC.steps.slice(0, FC.current).forEach((s, i) => { const el = document.createElement('div'); el.className = 'focus-done-step'; el.innerHTML = ` ${s.action}`; hist.appendChild(el); }); } async function focusNextStep() { clearInterval(FC._interval); FC.timerRunning = false; const btn = document.querySelector('#focus-session button[onclick="focusNextStep()"]'); btn.textContent = 'Getting next step…'; btn.disabled = true; const step = await focusFetchStep(FC.task, FC.steps); btn.textContent = '✓ Done — give me next step'; btn.disabled = false; if (!step) return; FC.current++; FC.steps.push(step); focusRenderStep(); toast('✓ Step complete! Keep going.'); } function focusTimerToggle() { if (FC.timerRunning) { clearInterval(FC._interval); FC.timerRunning = false; document.getElementById('focus-timer-btn').textContent = 'Resume'; } else { FC.timerRunning = true; document.getElementById('focus-timer-btn').textContent = 'Pause'; FC._interval = setInterval(() => { FC.timerSecs--; focusTimerUpdate(); if (FC.timerSecs <= 0) { clearInterval(FC._interval); FC.timerRunning = false; document.getElementById('focus-timer-btn').textContent = 'Restart'; toast('⏱ Time\'s up — did you complete the step?'); if (Notification.permission === 'granted') new Notification('Study Hub', { body: '⏱ Focus step time\'s up!' }); } }, 1000); } } function focusTimerUpdate() { const m = Math.floor(FC.timerSecs / 60).toString().padStart(2,'0'); const s = (FC.timerSecs % 60).toString().padStart(2,'0'); document.getElementById('focus-timer-display').textContent = `${m}:${s}`; document.getElementById('focus-timer-display').style.color = FC.timerSecs < 30 ? 'var(--red)' : 'var(--text)'; } function focusReset() { clearInterval(FC._interval); FC.steps = []; FC.current = 0; FC.timerRunning = false; document.getElementById('focus-session').style.display = 'none'; document.getElementById('focus-setup').style.display = 'flex'; document.getElementById('focus-task-input').value = ''; } init();