Skibidi Study
Skibidi Study
by Glass Cannon
Sources
Tools
System
⏱️ Session Timer
FOCUS
25:00
🍅 0 sessions today
Glass Cannon GLASS CANNON
ACTIVE DOC
Connections
Connect your document sources and learning tools
Connect your document sources
🔑 Setup Required: To connect Google Docs or Microsoft (OneNote/Word), you need to register OAuth apps and enter your Client IDs in Settings. Then use the Connect buttons below. See the Setup Guide in Settings for instructions.
📄
Google Docs
Disconnected
Read your Google Docs assignments and notes directly. Requires Google OAuth.
🪟
OneNote & Word
Disconnected
Access OneNote notebooks and Word Online documents via Microsoft Graph API.
🎓
Canvas LMS
Bookmarklet
Use the Canvas bookmarklet to inject the Study Hub panel directly on any Canvas page.
🔖 Drag to Bookmarks Bar
✓
Connected Accounts
My Documents
Select a document to analyze
📂
No documents yet
Connect Google Docs or Microsoft, or paste a document URL in Connections.
Analyze
No document selected
🔬
Select a document first
Go to My Documents, pick an assignment, then come back to analyze it.
Quick Tools
Document Preview
Result
Click a tool to analyze your document
0 / 0
🔍
Click "Parse Questions"
Claude will extract every question from the document and analyze each one.
←
Select a question
Click any question on the left to see its full breakdown.
Chat
General — no document loaded
💬
Ready to help
Ask anything about your assignment, or load a document for context-aware answers.
Assignment Tracker
Deadlines, kanban, subject detection
UPCOMING:
No upcoming deadlines — add an assignment above.
📎
Class Notes Linked
Your class notes are attached to this assignment.
id="kanban-board" style="flex:1;display:grid;grid-template-columns:1fr 1fr;gap:0;overflow:hidden;">
TO DO 0
LATE 0
Add Assignment
Grade Analyzer
What-if scenarios & assignment priority
🔑 Connect Canvas — 30 seconds
  1. In Canvas: click your profile picture → Settings
  2. Scroll to Approved Integrations → click + New Access Token
  3. Purpose: Study Hub · No expiry needed · click Generate Token
  4. Copy the token and paste it below with your Canvas domain
Canvas Connected
Select a course
Loading courses…
🎯 Priority List — Assignments ranked by grade impact
📋 All Assignments
Quiz Prep
AI-generated practice questions from your material
Q1 of 10
Score: 0/0
Citation Manager
Auto-format sources in APA, MLA, or Chicago
📚
No sources yet
Paste a URL, DOI, ISBN, or describe a source above. Claude will format it and extract a usable quote.
🎯 Focus Mode
Break your task into one tiny first step
🎯
What are you avoiding right now?
Type the assignment or task you've been putting off. Claude will break it into one step so small you can start in under 2 minutes.
Your one step right now
02:00
Notes
Private, saved locally
AI Summary
Settings
API keys and preferences
Theme
Choose your color theme
Claude API Key
Your Anthropic key — stored locally only
Canvas URL
Your school's Canvas domain (e.g. canvas.school.edu)
Canvas API Token
Account → Settings → Approved Integrations → New Access Token
n8n Proxy URL
Your n8n domain — enables Canvas grades without CORS issues
Google Client ID
From Google Cloud Console → OAuth 2.0 Client ID (Web app)
Microsoft Client ID
From Azure Portal → App registrations → Application (client) ID
App URL (for OAuth redirect)
The URL where this app is hosted — must match your OAuth redirect URI
📋
OAuth Setup Guide
// ── THEME SYSTEM ────────────────────────────────────────────── function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('sh_theme', theme); ['dark','light','rose','ocean','lavender'].forEach(t => { const btn = document.getElementById('theme-' + t); if (btn) btn.style.borderColor = t === theme ? 'var(--accent)' : 'transparent'; }); } function loadTheme() { const saved = localStorage.getItem('sh_theme') || 'dark'; document.documentElement.setAttribute('data-theme', saved); setTimeout(() => setTheme(saved), 50); } // ── SKELETON LOADER ──────────────────────────────────────────── function skeletonRows(n=3, h='48px') { return Array(n).fill('').map(() => `
` ).join(''); } // ── CHAT CHIPS ──────────────────────────────────────────────── function chatChip(prompt) { const input = document.getElementById('chat-input'); if (input) { input.value = prompt; sendChat(); } } // ── STATE ─────────────────────────────────────────────────── const S = { claudeKey: localStorage.getItem('sh_claude') || '', canvasUrl: localStorage.getItem('sh_canvas_url') || '', canvasToken: localStorage.getItem('sh_canvas_token') || '', n8nUrl: localStorage.getItem('sh_n8n_url') || '', useProxy: localStorage.getItem('sh_use_proxy') === 'true', googleToken: sessionStorage.getItem('sh_gtoken') || '', msToken: sessionStorage.getItem('sh_mstoken') || '', googleClient: localStorage.getItem('sh_gclient') || '', msClient: localStorage.getItem('sh_msclient') || '', appUrl: localStorage.getItem('sh_appurl') || window.location.origin + window.location.pathname, docs: JSON.parse(localStorage.getItem('sh_docs') || '[]'), activeDoc: null, chatHistory: [], notes: localStorage.getItem('sh_notes') || '', lastResult: '', currentView: 'sources', }; const CLAUDE_MODEL = 'claude-opus-4-5'; // ── PROXY HELPERS ─────────────────────────────────────────── async function claudeFetch(body) { if (S.n8nUrl) { const res = await fetch(`${S.n8nUrl}/webhook/claude`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body }), }); if (!res.ok) throw new Error(`Proxy error ${res.status}`); return res.json(); } if (!S.claudeKey) throw new Error('No Claude API key — add it in Settings'); const data = await claudeFetch(body); } // ── INIT ──────────────────────────────────────────────────── // ── THEME SYSTEM ───────────────────────────────────────────── function init() { try { loadTheme(); } catch(e) {} loadSettings(); document.getElementById('notes-textarea').value = S.notes; checkOAuthCallback(); renderDocsList(); setView('sources'); buildCanvasBookmarklet(); trackerRender(); timerUpdate(); const ci = document.getElementById('chat-input'); ci.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } }); ci.addEventListener('input', () => { ci.style.height = 'auto'; ci.style.height = Math.min(ci.scrollHeight, 120) + 'px'; }); // Keyboard shortcut: Enter in modal title field document.getElementById('ta-title').addEventListener('keydown', e => { if (e.key === 'Enter') trackerSaveAdd(); }); } // ── VIEW ROUTING ──────────────────────────────────────────── function setView(v) { S.currentView = v; document.querySelectorAll('.view').forEach(el => el.style.display = 'none'); document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); const el = document.getElementById('view-' + v); if (el) el.style.display = 'flex'; if (v === 'tracker') { setTimeout(trackerRender, 0); } if (v === 'grades') { setTimeout(gradesInitView, 0); } if (v === 'citations') { setTimeout(citationRender, 0); } const nav = document.getElementById('nav-' + v); if (nav) nav.classList.add('active'); if (v === 'analyze') renderAnalyzeView(); if (v === 'docs') renderDocsList(); if (v === 'chat') updateChatSub(); } // ── SETTINGS ──────────────────────────────────────────────── function loadSettings() { document.getElementById('s-claude-key').value = S.claudeKey; document.getElementById('s-canvas-url').value = S.canvasUrl; document.getElementById('s-canvas-token').value = S.canvasToken; if(document.getElementById('s-n8n-url')) document.getElementById('s-n8n-url').value = S.n8nUrl; document.getElementById('s-google-client').value = S.googleClient; document.getElementById('s-ms-client').value = S.msClient; document.getElementById('s-app-url').value = S.appUrl; } function saveSettings() { S.claudeKey = document.getElementById('s-claude-key').value.trim(); S.canvasUrl = document.getElementById('s-canvas-url').value.trim().replace(/^https?:\/\//, '').replace(/\/$/, ''); S.canvasToken = document.getElementById('s-canvas-token').value.trim(); S.n8nUrl = (document.getElementById('s-n8n-url')?.value||'').trim().replace(/\/$/, ''); S.useProxy = !!S.n8nUrl; localStorage.setItem('sh_n8n_url', S.n8nUrl); localStorage.setItem('sh_use_proxy', S.useProxy); S.googleClient = document.getElementById('s-google-client').value.trim(); S.msClient = document.getElementById('s-ms-client').value.trim(); S.appUrl = document.getElementById('s-app-url').value.trim() || S.appUrl; localStorage.setItem('sh_claude', S.claudeKey); localStorage.setItem('sh_canvas_url', S.canvasUrl); localStorage.setItem('sh_canvas_token', S.canvasToken); localStorage.setItem('sh_gclient', S.googleClient); localStorage.setItem('sh_msclient', S.msClient); localStorage.setItem('sh_appurl', S.appUrl); toast('✓ Settings saved'); buildCanvasBookmarklet(); } // ── GOOGLE OAUTH ───────────────────────────────────────────── function connectGoogle() { if (!S.googleClient) { toast('⚠ Enter Google Client ID in Settings first'); setView('settings'); return; } const params = new URLSearchParams({ client_id: S.googleClient, redirect_uri: S.appUrl, response_type: 'token', scope: 'https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.readonly', state: 'google', }); window.location.href = 'https://accounts.google.com/o/oauth2/v2/auth?' + params; } async function fetchGoogleDocs() { if (!S.googleToken) return []; try { const res = await fetch( "https://www.googleapis.com/drive/v3/files?q=mimeType='application/vnd.google-apps.document'&fields=files(id,name,modifiedTime)&pageSize=30", { headers: { Authorization: 'Bearer ' + S.googleToken } } ); const data = await res.json(); if (data.error) { S.googleToken = ''; sessionStorage.removeItem('sh_gtoken'); return []; } return (data.files || []).map(f => ({ id: 'g_' + f.id, source: 'google', name: f.name, rawId: f.id, meta: 'Google Doc', icon: '📄', modified: f.modifiedTime, })); } catch { return []; } } async function fetchGoogleDocContent(rawId) { const res = await fetch( `https://docs.googleapis.com/v1/documents/${rawId}`, { headers: { Authorization: 'Bearer ' + S.googleToken } } ); const data = await res.json(); if (data.error) return null; let text = ''; const body = data.body?.content || []; for (const el of body) { if (el.paragraph) { for (const pe of (el.paragraph.elements || [])) { text += (pe.textRun?.content || ''); } } } return { title: data.title, content: text.trim() }; } // ── MICROSOFT OAUTH ────────────────────────────────────────── function connectMicrosoft() { if (!S.msClient) { toast('⚠ Enter Microsoft Client ID in Settings first'); setView('settings'); return; } const params = new URLSearchParams({ client_id: S.msClient, redirect_uri: S.appUrl, response_type: 'token', scope: 'Files.Read Notes.Read User.Read', state: 'microsoft', }); window.location.href = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + params; } async function fetchMsDocs() { if (!S.msToken) return []; try { const [filesRes, notesRes] = await Promise.allSettled([ fetch('https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=file ne null&$select=id,name,lastModifiedDateTime,file&$top=30', { headers: { Authorization: 'Bearer ' + S.msToken } }), fetch('https://graph.microsoft.com/v1.0/me/onenote/pages?$select=id,title,lastModifiedDateTime&$top=30', { headers: { Authorization: 'Bearer ' + S.msToken } }), ]); let docs = []; if (filesRes.status === 'fulfilled') { const data = await filesRes.value.json(); const wordFiles = (data.value || []).filter(f => f.file?.mimeType?.includes('word') || f.name?.endsWith('.docx')); docs.push(...wordFiles.map(f => ({ id: 'ms_' + f.id, source: 'word', name: f.name, rawId: f.id, meta: 'Word Doc', icon: '📘', modified: f.lastModifiedDateTime, }))); } if (notesRes.status === 'fulfilled') { const data = await notesRes.value.json(); docs.push(...(data.value || []).map(f => ({ id: 'on_' + f.id, source: 'onenote', name: f.title || 'Untitled', rawId: f.id, meta: 'OneNote', icon: '🗒', modified: f.lastModifiedDateTime, }))); } return docs; } catch { return []; } } async function fetchMsDocContent(doc) { if (doc.source === 'onenote') { const res = await fetch(`https://graph.microsoft.com/v1.0/me/onenote/pages/${doc.rawId}/content`, { headers: { Authorization: 'Bearer ' + S.msToken } }); const html = await res.text(); const tmp = document.createElement('div'); tmp.innerHTML = html; return { title: doc.name, content: tmp.innerText?.trim() || '' }; } else { // Word: get download URL const res = await fetch(`https://graph.microsoft.com/v1.0/me/drive/items/${doc.rawId}?$select=@microsoft.graph.downloadUrl,name`, { headers: { Authorization: 'Bearer ' + S.msToken } }); const data = await res.json(); return { title: data.name, content: '[Word document detected — text extraction requires server-side processing. Use the content preview from OneDrive or paste the text manually.]' }; } } // ── OAUTH CALLBACK HANDLER ─────────────────────────────────── function checkOAuthCallback() { const hash = window.location.hash; if (!hash.includes('access_token')) return; const params = new URLSearchParams(hash.substring(1)); const token = params.get('access_token'); const state = params.get('state'); if (!token) return; history.replaceState(null, '', window.location.pathname); if (state === 'google') { S.googleToken = token; sessionStorage.setItem('sh_gtoken', token); updateConnectionStatus(); toast('✓ Google connected! Loading docs…'); refreshDocs(); setView('docs'); } else if (state === 'microsoft') { S.msToken = token; sessionStorage.setItem('sh_mstoken', token); updateConnectionStatus(); toast('✓ Microsoft connected! Loading docs…'); refreshDocs(); setView('docs'); } } function updateConnectionStatus() { const gs = document.getElementById('google-status'); const ms = document.getElementById('ms-status'); if (S.googleToken) { gs.textContent = 'Connected'; gs.className = 'source-status status-connected'; document.getElementById('google-card').classList.add('connected'); } else { gs.textContent = 'Disconnected'; gs.className = 'source-status status-disconnected'; } if (S.msToken) { ms.textContent = 'Connected'; ms.className = 'source-status status-connected'; document.getElementById('ms-card').classList.add('connected'); } else { ms.textContent = 'Disconnected'; ms.className = 'source-status status-disconnected'; } } // ── MANUAL URL PASTING ─────────────────────────────────────── function manualGooglePaste() { const s = document.getElementById('google-url-strip'); s.style.display = s.style.display === 'none' ? 'block' : 'none'; } function manualMsPaste() { const s = document.getElementById('ms-url-strip'); s.style.display = s.style.display === 'none' ? 'block' : 'none'; } async function loadGoogleDocByUrl() { const url = document.getElementById('google-url-input').value.trim(); const match = url.match(/\/d\/([a-zA-Z0-9_-]+)/); if (!match) { toast('⚠ Invalid Google Docs URL'); return; } const id = match[1]; const docMeta = { id: 'g_' + id, source: 'google', name: 'Google Doc (from URL)', rawId: id, meta: 'Google Doc', icon: '📄', needsToken: true }; if (S.googleToken) { toast('Loading doc…'); const content = await fetchGoogleDocContent(id); if (content) { docMeta.name = content.title; docMeta.content = content.content; } } else { docMeta.content = '[Connect Google account to read full content, or paste the document text manually in the Chat tab]'; } addDoc(docMeta); toast('✓ Document added'); setView('docs'); } async function loadMsDocByUrl() { const url = document.getElementById('ms-url-input').value.trim(); if (!url) return; const doc = { id: 'ms_manual_' + Date.now(), source: 'word', name: 'Microsoft Document', rawId: null, meta: 'Word/OneNote', icon: '🪟', content: '[Paste document text in Chat → Context tab, or connect Microsoft account for automatic reading]' }; addDoc(doc); toast('✓ Document added'); setView('docs'); } // ── DOC MANAGEMENT ─────────────────────────────────────────── function addDoc(doc) { const existing = S.docs.findIndex(d => d.id === doc.id); if (existing >= 0) S.docs[existing] = doc; else S.docs.unshift(doc); localStorage.setItem('sh_docs', JSON.stringify(S.docs)); } async function refreshDocs() { toast('Refreshing documents…'); const [gDocs, msDocs] = await Promise.all([ S.googleToken ? fetchGoogleDocs() : [], S.msToken ? fetchMsDocs() : [], ]); const existing = S.docs.filter(d => d.source === 'manual' || d.source === 'canvas'); S.docs = [...gDocs, ...msDocs, ...existing]; localStorage.setItem('sh_docs', JSON.stringify(S.docs)); renderDocsList(); toast(`✓ ${S.docs.length} documents loaded`); updateConnectionStatus(); } let docFilter = 'all'; function filterDocs(f, btn) { docFilter = f; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); renderDocsList(); } function renderDocsList() { const container = document.getElementById('docs-list-container'); let docs = S.docs; if (docFilter !== 'all') docs = docs.filter(d => d.source === docFilter); if (!docs.length) { container.innerHTML = `
📂
No documents
${docFilter === 'all' ? 'Connect a source or paste a document URL in Connections.' : 'No ' + docFilter + ' documents found.'}
`; return; } const list = document.createElement('div'); list.className = 'doc-list'; docs.forEach(doc => { const item = document.createElement('div'); item.className = 'doc-item' + (S.activeDoc?.id === doc.id ? ' selected' : ''); item.innerHTML = ` ${doc.icon} ${doc.name} ${doc.meta} `; item.onclick = () => selectDoc(doc); list.appendChild(item); }); container.innerHTML = ''; container.appendChild(list); } async function selectDoc(doc) { S.activeDoc = doc; // Load content if not loaded if (!doc.content && S.googleToken && doc.source === 'google') { toast('Loading document content…'); const c = await fetchGoogleDocContent(doc.rawId); if (c) { doc.content = c.content; doc.name = c.title; addDoc(doc); } } else if (!doc.content && S.msToken && (doc.source === 'onenote' || doc.source === 'word')) { toast('Loading document content…'); const c = await fetchMsDocContent(doc); if (c) { doc.content = c.content; addDoc(doc); } } document.getElementById('analyze-selected-btn').disabled = false; document.getElementById('active-doc-pill').style.display = 'block'; document.getElementById('active-doc-name').textContent = doc.name; renderDocsList(); toast('✓ ' + doc.name + ' selected'); } function analyzeSelected() { if (!S.activeDoc) return; setView('analyze'); } // ── ANALYZE VIEW ───────────────────────────────────────────── const TOOLS = [ { id: 'summarize', icon: '📋', label: 'Summarize', desc: 'TL;DR of key points', prompt: 'Summarize this document concisely. Extract the main goal, requirements, and key points.' }, { id: 'breakdown', icon: '🪜', label: 'Step-by-Step', desc: 'Numbered action items', prompt: 'Break this assignment into clear, numbered actionable steps. Make each step specific and achievable.' }, { id: 'simplify', icon: '💡', label: 'Simplify', desc: 'Plain English rewrite', prompt: 'Rewrite these instructions in simple, plain English. Remove all jargon and academic language.' }, { id: 'rubric', icon: '✅', label: 'Rubric Guide', desc: 'How to get full marks', prompt: 'Based on the requirements and rubric in this document, tell me exactly what to do to score full marks. List each criterion.' }, { id: 'outline', icon: '📝', label: 'Outline', desc: 'Writing structure', prompt: 'Create a detailed essay/assignment outline with sections and sub-bullets for what to cover in each.' }, { id: 'schedule', icon: '⏱️', label: 'Time Plan', desc: 'Study schedule', prompt: 'Create a realistic Pomodoro-style study schedule to complete this assignment. Include time estimates for each section.' }, { id: 'keyterms', icon: '🔑', label: 'Key Terms', desc: 'Important vocab/concepts', prompt: 'Extract and define all key terms, concepts, formulas, or important vocabulary from this document.' }, { id: 'questions', icon: '❓', label: 'Practice Qs', desc: 'Study questions', prompt: 'Generate 5 practice questions based on this document that would test understanding of the material.' }, ]; // ── ANALYZE TAB SWITCHING ──────────────────────────────────── function switchAnalyzeTab(tab) { ['tools','qb'].forEach(t => { document.getElementById('apanel-' + t).style.display = t === tab ? 'flex' : 'none'; document.getElementById('atab-' + t).classList.toggle('active', t === tab); }); const isTool = tab === 'tools'; document.getElementById('btn-copy-result').style.display = isTool && S.lastResult ? 'inline-flex' : 'none'; document.getElementById('btn-save-notes').style.display = isTool && S.lastResult ? 'inline-flex' : 'none'; document.getElementById('qb-save-all-btn').style.display = !isTool && QB.questions.length ? 'inline-flex' : 'none'; } function renderAnalyzeView() { const main = document.getElementById('analyze-main'); const noDoc = document.getElementById('no-doc-state'); const title = document.getElementById('analyze-doc-title'); if (!S.activeDoc) { main.style.display = 'none'; noDoc.style.display = 'flex'; return; } main.style.display = 'flex'; noDoc.style.display = 'none'; title.textContent = S.activeDoc.name; const grid = document.getElementById('tool-grid'); grid.innerHTML = ''; TOOLS.forEach(t => { const chip = document.createElement('div'); chip.className = 'tool-chip'; chip.id = 'chip-' + t.id; chip.innerHTML = `
${t.icon}
${t.label}
${t.desc}
`; chip.onclick = () => runTool(t); grid.appendChild(chip); }); const preview = document.getElementById('doc-preview'); preview.textContent = (S.activeDoc.content || 'No content loaded.').slice(0, 2000) + (S.activeDoc.content?.length > 2000 ? '…' : ''); resetResultBox(); // keep whichever tab was last open switchAnalyzeTab(QB._activeTab || 'tools'); } function resetResultBox() { const rb = document.getElementById('result-box'); rb.className = 'result-box empty'; rb.innerHTML = 'Click a tool to analyze your document'; document.getElementById('result-actions').style.display = 'none'; const rl = document.getElementById('result-label'); rl.querySelector('.dot').classList.remove('pulse'); } // ── QUESTION BREAKDOWN ENGINE ──────────────────────────────── const QB = { questions: [], // [{id,type,text,points,subItems}] analyses: {}, // {id: {what,how,tips,points,done}} activeId: null, parsing: false, _activeTab:'tools', }; const QB_PARSE_SYSTEM = `You are an academic document parser. Extract every distinct question, task, or deliverable from the assignment. Output ONLY valid JSON — no markdown, no explanation. Format: [ { "id": 1, "type": "question|task|instruction", "text": "exact question text", "points": null or number, "subItems": ["sub-part a text", "sub-part b text"] } ] Rules: - Capture EVERY numbered or lettered item - subItems: only if the question has lettered sub-parts (a, b, c) - type "instruction" = general directions, not a specific answerable question - Preserve exact wording`; async function qbParseDoc() { if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); return; } if (!S.activeDoc?.content) { toast('⚠ No document content to parse'); return; } if (QB.parsing) return; QB.parsing = true; QB.questions = []; QB.analyses = {}; QB.activeId = null; QB._activeTab = 'qb'; const btn = document.getElementById('qb-parse-btn'); btn.disabled = true; btn.textContent = 'Parsing…'; document.getElementById('qb-status').textContent = 'Extracting questions…'; document.getElementById('qb-progress-wrap').style.display = 'flex'; document.getElementById('qb-save-all-btn').style.display = 'none'; qbRenderList([]); qbShowEmpty(); try { const data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 2000, system: QB_PARSE_SYSTEM, messages: [{ role:'user', content:`Title: ${S.activeDoc.name}\n\n${S.activeDoc.content.slice(0,10000)}` }], }); const raw = data?.content?.[0]?.text || '[]'; const clean = raw.replace(/```json|```/g,'').trim(); QB.questions = JSON.parse(clean); } catch(e) { toast('Error parsing: ' + e.message); QB.parsing = false; btn.disabled = false; btn.textContent = '✦ Parse Questions'; document.getElementById('qb-status').textContent = 'Parse failed'; return; } if (!QB.questions.length) { toast('No questions found in document'); QB.parsing = false; btn.disabled = false; btn.textContent = '✦ Parse Questions'; document.getElementById('qb-status').textContent = 'No questions detected'; return; } btn.textContent = '↻ Re-Parse'; btn.disabled = false; document.getElementById('qb-status').textContent = `${QB.questions.length} question${QB.questions.length!==1?'s':''} found`; qbRenderList(QB.questions); // Now analyze each question one by one QB.parsing = true; for (let i = 0; i < QB.questions.length; i++) { const q = QB.questions[i]; const pct = Math.round((i / QB.questions.length) * 100); document.getElementById('qb-progress-bar').style.width = pct + '%'; document.getElementById('qb-progress-label').textContent = `${i} / ${QB.questions.length}`; document.getElementById('qb-status').textContent = `Analyzing Q${q.id}…`; qbUpdateListItem(q.id, 'analyzing'); await qbAnalyzeQuestion(q); qbUpdateListItem(q.id, 'done'); // Auto-select first one when ready if (i === 0) { QB.activeId = q.id; qbRenderDetail(q.id); } } document.getElementById('qb-progress-bar').style.width = '100%'; document.getElementById('qb-progress-label').textContent = `${QB.questions.length} / ${QB.questions.length}`; document.getElementById('qb-status').textContent = `All ${QB.questions.length} questions analyzed ✓`; document.getElementById('qb-save-all-btn').style.display = 'inline-flex'; QB.parsing = false; } async function qbAnalyzeQuestion(q) { const subCtx = q.subItems?.length ? `\nSub-parts:\n${q.subItems.map((s,i)=>`${String.fromCharCode(97+i)}) ${s}`).join('\n')}` : ''; const prompt = `Analyze this assignment question and give a structured breakdown for the student. Question ${q.id}${q.points?` (${q.points} pts)`:''}: ${q.text}${subCtx} Full assignment context: ${(S.activeDoc?.content||'').slice(0,5000)} Respond in this EXACT format (use the exact headers): ## What is being asked [1-2 sentences in plain English explaining what the question wants] ## How to answer it [Numbered steps — the actual approach/method to tackle this question] ## Key things to include [Bullet points of specific content, evidence, or concepts needed for a strong answer] ## Watch out for [1-3 common mistakes or tricky aspects of this question] ${q.points ? `## Points breakdown\n[How marks are likely distributed across this question]` : ''}`; try { const data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 900, system: 'You are a sharp academic tutor. Be concise, specific, and genuinely useful. Format with markdown.', messages: [{ role:'user', content: prompt }], }); const reply = data?.content?.[0]?.text || 'No response.'; QB.analyses[q.id] = { text: reply, done: true }; } catch(e) { QB.analyses[q.id] = { text: 'Error analyzing this question: ' + e.message, done: true }; } } // ── QB UI HELPERS ──────────────────────────────────────────── function qbRenderList(questions) { const list = document.getElementById('qb-list'); if (!questions.length) { list.innerHTML = `
🔍
Click "Parse Questions"
Claude will extract and analyze every question.
`; return; } list.innerHTML = ''; questions.forEach(q => { const item = document.createElement('div'); item.id = 'qb-item-' + q.id; item.style.cssText = 'padding:10px 12px; border-radius:10px; border:1px solid var(--border); background:rgba(255,255,255,0.02); margin-bottom:6px; cursor:pointer; transition:all 0.15s;'; item.innerHTML = qbListItemHTML(q, 'pending'); item.onclick = () => { QB.activeId = q.id; qbRenderDetail(q.id); qbHighlightItem(q.id); }; list.appendChild(item); }); } function qbListItemHTML(q, state) { const typeCol = {question:'var(--blue)',task:'#C084FC',instruction:'var(--amber)'}[q.type] || 'var(--muted)'; const dot = state === 'analyzing' ? `
` : state === 'done' ? `
` : `
`; return `
${dot} Q${q.id}
${q.text}
${q.points ? `
${q.points} pts
` : ''}
`; } function qbUpdateListItem(id, state) { const el = document.getElementById('qb-item-' + id); const q = QB.questions.find(q => q.id === id); if (!el || !q) return; el.innerHTML = qbListItemHTML(q, state); if (state === 'done' && QB.activeId === id) qbRenderDetail(id); } function qbHighlightItem(id) { document.querySelectorAll('#qb-list > div').forEach(el => { el.style.background = 'rgba(255,255,255,0.02)'; el.style.borderColor = 'var(--border)'; }); const el = document.getElementById('qb-item-' + id); if (el) { el.style.background = 'rgba(94,234,212,0.07)'; el.style.borderColor = 'rgba(94,234,212,0.25)'; } } function qbShowEmpty() { document.getElementById('qb-detail-empty').style.display = 'flex'; document.getElementById('qb-detail-content').style.display = 'none'; } function qbRenderDetail(id) { const q = QB.questions.find(q => q.id === id); const ana = QB.analyses[id]; const empty = document.getElementById('qb-detail-empty'); const content = document.getElementById('qb-detail-content'); qbHighlightItem(id); empty.style.display = 'none'; content.style.display = 'flex'; const typeCol = {question:'var(--blue)',task:'#C084FC',instruction:'var(--amber)'}[q.type] || 'var(--muted)'; content.innerHTML = `
${q.type} ${q.points ? `${q.points} pts` : ''} Q${q.id} of ${QB.questions.length}
${q.text}
${q.subItems?.length ? `
${q.subItems.map((s,i)=>`
${String.fromCharCode(97+i)})${s}
`).join('')}
` : ''}
${ana?.done ? `
Breakdown
${formatMd(ana.text)}
` : `
Analyzing question ${id}…
` }
${id > 1 ? `` : ''} ${id < QB.questions.length ? `` : ''}
`; } function qbNav(id) { QB.activeId = id; qbRenderDetail(id); qbHighlightItem(id); } function qbCopyAnalysis(id) { const ana = QB.analyses[id]; if (ana?.text) { navigator.clipboard.writeText(ana.text); toast('✓ Copied'); } } function qbSaveOne(id) { const q = QB.questions.find(q => q.id === id); const ana = QB.analyses[id]; if (!q || !ana?.text) return; const ta = document.getElementById('notes-textarea'); ta.value += `\n\n── Q${q.id}: ${q.text.slice(0,60)}… ──\n${ana.text}`; S.notes = ta.value; localStorage.setItem('sh_notes', S.notes); toast('✓ Saved to Notes'); } function qbSaveAll() { const ta = document.getElementById('notes-textarea'); let block = `\n\n════ ${S.activeDoc?.name || 'Assignment'} — Question Breakdowns ════\n`; QB.questions.forEach(q => { const ana = QB.analyses[q.id]; if (ana?.text) block += `\n── Q${q.id}: ${q.text.slice(0,80)}${q.text.length>80?'…':''} ──\n${ana.text}\n`; }); ta.value += block; S.notes = ta.value; localStorage.setItem('sh_notes', S.notes); toast(`✓ All ${QB.questions.length} breakdowns saved to Notes`); } async function runTool(tool) { if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); return; } if (!S.activeDoc?.content) { toast('⚠ Document has no content to analyze'); return; } document.querySelectorAll('.tool-chip').forEach(c => c.classList.remove('active', 'running')); const chip = document.getElementById('chip-' + tool.id); chip.classList.add('active', 'running'); const rb = document.getElementById('result-box'); rb.className = 'result-box'; rb.innerHTML = `
Analyzing with Claude…
`; document.getElementById('result-actions').style.display = 'none'; document.getElementById('result-label').querySelector('.dot').classList.add('pulse'); const docContext = `Document: "${S.activeDoc.name}" (Source: ${S.activeDoc.source})\n\n${S.activeDoc.content.slice(0, 8000)}`; const systemPrompt = `You are a sharp academic AI assistant. The student has provided a document from their course. Be concise, structured, and student-focused. Use **bold**, numbered lists, and bullet points to format your response clearly.`; try { const data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 1500, system: systemPrompt, messages: [{ role: 'user', content: `${tool.prompt}\n\n---\n${docContext}` }], }); const reply = data?.content?.[0]?.text || 'No response.'; S.lastResult = reply; rb.className = 'result-box'; rb.innerHTML = formatMd(reply); document.getElementById('result-actions').style.display = 'flex'; document.getElementById('btn-copy-result').style.display = 'inline-flex'; document.getElementById('btn-save-notes').style.display = 'inline-flex'; } catch(e) { rb.innerHTML = `Error: ${e.message}`; } chip.classList.remove('running'); document.getElementById('result-label').querySelector('.dot').classList.remove('pulse'); } function copyResult() { if (!S.lastResult) return; navigator.clipboard.writeText(S.lastResult); toast('✓ Copied to clipboard'); } function saveToNotes() { if (!S.lastResult) return; const existing = document.getElementById('notes-textarea').value; const header = `\n\n── ${S.activeDoc?.name || 'Document'} ──\n`; document.getElementById('notes-textarea').value = existing + header + S.lastResult; S.notes = document.getElementById('notes-textarea').value; localStorage.setItem('sh_notes', S.notes); toast('✓ Saved to Notes'); } function sendToChat() { if (!S.lastResult) return; S.chatHistory.push({ role: 'assistant', content: '**From Analyze:**\n\n' + S.lastResult }); setView('chat'); renderChat(); } // ── CHAT ──────────────────────────────────────────────────── function updateChatSub() { const sub = document.getElementById('chat-doc-sub'); sub.textContent = S.activeDoc ? S.activeDoc.name + ' — context loaded' : 'General — no document loaded'; } function clearChat() { S.chatHistory = []; renderChat(); } function renderChat() { const msgs = document.getElementById('chat-msgs'); const empty = document.getElementById('chat-empty'); msgs.innerHTML = ''; if (!S.chatHistory.length) { msgs.appendChild(empty); return; } S.chatHistory.forEach(m => { const row = document.createElement('div'); row.className = 'msg-row ' + m.role; const av = document.createElement('div'); av.className = 'msg-avatar ' + (m.role === 'assistant' ? 'ai' : 'user'); av.textContent = m.role === 'assistant' ? 'S' : '👤'; const col = document.createElement('div'); col.style.cssText = 'display:flex;flex-direction:column;max-width:78%;'; const bub = document.createElement('div'); bub.className = 'bubble ' + (m.role === 'assistant' ? 'ai' : 'user'); bub.innerHTML = m.role === 'assistant' ? formatMd(m.content) : m.content; col.appendChild(bub); if (m.role === 'assistant') { const cb = document.createElement('button'); cb.className = 'msg-copy-btn'; cb.textContent = '⎘ Copy'; cb.onclick = () => { navigator.clipboard.writeText(m.content); cb.textContent = '✓ Copied'; setTimeout(() => cb.textContent = '⎘ Copy', 1500); }; col.appendChild(cb); } row.appendChild(av); row.appendChild(col); msgs.appendChild(row); }); msgs.scrollTop = msgs.scrollHeight; } async function sendChat() { const input = document.getElementById('chat-input'); const text = input.value.trim(); if (!text || !S.claudeKey) { if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); } return; } input.value = ''; input.style.height = 'auto'; S.chatHistory.push({ role: 'user', content: text }); renderChat(); document.getElementById('chat-send-btn').disabled = true; const docCtx = S.activeDoc?.content ? `\n\nThe student has this document loaded: "${S.activeDoc.name}"\n---\n${S.activeDoc.content.slice(0, 6000)}` : ''; // Show typing indicator const msgs = document.getElementById('chat-msgs'); const typing = document.createElement('div'); typing.id = 'chat-typing'; typing.className = 'msg-row assistant'; typing.innerHTML = '
S
'; msgs.appendChild(typing); msgs.scrollTop = msgs.scrollHeight; try { const data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 1500, system: 'You are Skibidi Study, a helpful AI tutor for students. Be clear, concise, and encouraging. Use markdown for formatting.' + docCtx, messages: S.chatHistory.map(m => ({ role: m.role, content: m.content })), }); const reply = data?.content?.[0]?.text || 'No response.'; document.getElementById('chat-typing')?.remove(); S.chatHistory.push({ role: 'assistant', content: reply }); } catch(e) { document.getElementById('chat-typing')?.remove(); S.chatHistory.push({ role: 'assistant', content: 'Error: ' + e.message }); } document.getElementById('chat-send-btn').disabled = false; renderChat(); } // ── NOTES ──────────────────────────────────────────────────── function saveNotes() { S.notes = document.getElementById('notes-textarea').value; localStorage.setItem('sh_notes', S.notes); toast('✓ Notes saved'); } async function aiSummarizeNotes() { const content = document.getElementById('notes-textarea').value; if (!content.trim()) { toast('⚠ Notes are empty'); return; } if (!S.claudeKey) { toast('⚠ Enter Claude API key in Settings'); setView('settings'); return; } const box = document.getElementById('notes-ai-box'); const result = document.getElementById('notes-ai-result'); result.style.display = 'block'; document.getElementById('notes-dot').classList.add('pulse'); box.innerHTML = '
'; try { const data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 600, messages: [{ role: 'user', content: 'Summarize these student notes concisely, pulling out key ideas and action items:\n\n' + content }], }); box.innerHTML = formatMd(data?.content?.[0]?.text || ''); } catch(e) { box.innerHTML = `${e.message}`; } document.getElementById('notes-dot').classList.remove('pulse'); } // ── CANVAS BOOKMARKLET ──────────────────────────────────────── function buildCanvasBookmarklet() { const appUrl = S.appUrl || window.location.href; const code = `(function(){ var e=document.createElement('script'); e.src='${appUrl.replace(/\/[^/]*$/, '')}/bookmarklet.js?t='+Date.now(); document.head.appendChild(e); })()`; const href = 'javascript:' + encodeURIComponent(code); document.getElementById('canvas-bookmarklet-link').href = href; } function copyCanvasBookmarklet() { const link = document.getElementById('canvas-bookmarklet-link').href; navigator.clipboard.writeText(link); toast('✓ Bookmarklet code copied'); } // ── MARKDOWN FORMATTER ──────────────────────────────────────── function formatMd(text) { return text .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/^### (.+)$/gm, '
$1
') .replace(/^## (.+)$/gm, '
$1
') .replace(/^- (.+)$/gm, '
• $1
') .replace(/^(\d+)\. (.+)$/gm, '
$1. $2
') .replace(/`([^`]+)`/g, '$1') .replace(/\n\n/g, '

').replace(/\n/g, '
'); } // ── TOAST ──────────────────────────────────────────────────── function toast(msg, type) { document.querySelectorAll('.toast').forEach(t => t.remove()); const t = document.createElement('div'); t.className = 'toast'; if (type === 'success') t.style.borderColor = 'rgba(93,187,107,0.5)'; if (type === 'error') t.style.borderColor = 'rgba(248,113,113,0.5)'; t.textContent = msg; document.body.appendChild(t); setTimeout(() => { t.style.transition = 'opacity 0.3s, transform 0.3s'; t.style.opacity = '0'; t.style.transform = 'translateX(-50%) translateY(8px)'; setTimeout(() => t.remove(), 300); }, 2500); } // ═══════════════════════════════════════════════════════════ // ── ASSIGNMENT TRACKER ────────────────────────────────────── // ═══════════════════════════════════════════════════════════ const TRACKER_KEY = 'sh_tracker'; let TR = { assignments: JSON.parse(localStorage.getItem(TRACKER_KEY) || '[]') }; const SUBJECT_MAP = { math: { label:'Math', icon:'📐', cls:'subj-math' }, science: { label:'Science', icon:'🔬', cls:'subj-science' }, biology: { label:'Biology', icon:'🧬', cls:'subj-science' }, chemistry:{ label:'Chemistry', icon:'⚗️', cls:'subj-science' }, physics: { label:'Physics', icon:'⚡', cls:'subj-science' }, history: { label:'History', icon:'📜', cls:'subj-history' }, english: { label:'English', icon:'📖', cls:'subj-english' }, writing: { label:'Writing', icon:'✍️', cls:'subj-english' }, cs: { label:'Comp Sci', icon:'💻', cls:'subj-cs' }, business: { label:'Business', icon:'📊', cls:'subj-business' }, economics:{ label:'Economics', icon:'📈', cls:'subj-business' }, art: { label:'Art', icon:'🎨', cls:'subj-art' }, other: { label:'General', icon:'📚', cls:'subj-other' }, }; // ── CANVAS ASSIGNMENT SYNC ─────────────────────────────────── async function trackerSyncCanvas() { if (!S.canvasToken || !S.canvasUrl) { toast('⚠ Connect Canvas in Settings first'); return; } const btn = document.getElementById('tracker-sync-btn'); if (btn) { btn.textContent = 'Syncing…'; btn.disabled = true; } try { // Get active courses const allCourses = await canvasFetch('/api/v1/courses?per_page=100&include[]=term&enrollment_type=student'); const now = new Date(); const activeCourses = (Array.isArray(allCourses) ? allCourses : []).filter(c => { if (!c.name || c.access_restricted_by_date || c.workflow_state !== 'available') return false; if (c.term?.name?.toLowerCase().includes('archive')) return false; if (c.term?.end_at && new Date(c.term.end_at) < new Date(now.getFullYear() - 1, 0, 1)) 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 data = await claudeFetch({ 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 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 data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 3000, messages: [{ role:'user', content: prompt }] }); 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 data = await claudeFetch({ 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 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 data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 500, messages:[{ role:'user', content: prompt }] }); 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 data = await claudeFetch({ model: CLAUDE_MODEL, max_tokens: 200, messages:[{ role:'user', content: prompt }] }); 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();