From 827a376018317f5ca2ad7084c70f20f8d7ac41fe Mon Sep 17 00:00:00 2001 From: Bhavika Patil Date: Thu, 28 May 2026 22:55:19 +0530 Subject: [PATCH] Add AI-powered Smart Study Planner --- css/planner.css | 89 +++++++++++++++ database.js | 33 ++++++ index.html | 160 +++++++++++++++++++++++++-- js/app.js | 4 +- js/confetti.browser.min.js | 8 ++ js/planner.js | 215 +++++++++++++++++++++++++++++++++++++ js/store.js | 112 ++++++++++++++++++- package-lock.json | 2 +- server.js | 138 +++++++++++++++++++++++- 9 files changed, 746 insertions(+), 15 deletions(-) create mode 100644 css/planner.css create mode 100644 js/confetti.browser.min.js create mode 100644 js/planner.js diff --git a/css/planner.css b/css/planner.css new file mode 100644 index 0000000..d6f316e --- /dev/null +++ b/css/planner.css @@ -0,0 +1,89 @@ +/* Smart Study Planner Styles */ + +.streak-widget { + color: var(--color-text-warning); +} + +.session-block { + background: var(--color-background-primary); + border: 1px solid var(--color-border-primary); + border-left: 4px solid var(--color-text-info); + border-radius: 8px; + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + transition: transform 0.2s, box-shadow 0.2s; +} + +.session-block:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-sm); +} + +.session-block.completed { + opacity: 0.6; + border-left-color: var(--color-text-success); +} + +.session-block.completed .session-title { + text-decoration: line-through; + color: var(--color-text-tertiary); +} + +.session-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.session-title { + font-weight: 600; + font-size: 16px; + color: var(--color-text-primary); +} + +.session-meta { + font-size: 13px; + color: var(--color-text-secondary); + display: flex; + gap: 12px; +} + +.session-type-badge { + background: var(--color-background-secondary); + padding: 2px 8px; + border-radius: 12px; + font-size: 11px; + text-transform: uppercase; + font-weight: 700; + letter-spacing: 0.05em; +} + +.session-type-revision { + color: var(--color-text-warning); + background: rgba(234, 179, 8, 0.1); +} + +.session-type-learning { + color: var(--color-text-info); + background: rgba(59, 130, 246, 0.1); +} + +.exam-item, .goal-item { + background: var(--color-background-secondary); + padding: 12px; + border-radius: 8px; + border: 1px solid var(--color-border-secondary); +} + +.exam-title, .goal-title { + font-weight: 600; + font-size: 14px; + margin-bottom: 4px; +} + +.exam-meta, .goal-meta { + font-size: 12px; + color: var(--color-text-tertiary); +} diff --git a/database.js b/database.js index 6fc7bec..a553765 100644 --- a/database.js +++ b/database.js @@ -40,6 +40,39 @@ function initDb() { } }); + // Exams Table + db.run(`CREATE TABLE IF NOT EXISTS exams ( + id TEXT PRIMARY KEY, + subject_id TEXT, + title TEXT NOT NULL, + date DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subject_id) REFERENCES subjects(id) + )`); + + // Study Goals Table + db.run(`CREATE TABLE IF NOT EXISTS study_goals ( + id TEXT PRIMARY KEY, + subject_id TEXT, + description TEXT NOT NULL, + target_date DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subject_id) REFERENCES subjects(id) + )`); + + // Study Sessions Table + db.run(`CREATE TABLE IF NOT EXISTS study_sessions ( + id TEXT PRIMARY KEY, + subject_id TEXT, + title TEXT NOT NULL, + start_time DATETIME NOT NULL, + end_time DATETIME NOT NULL, + status TEXT DEFAULT 'pending', + type TEXT DEFAULT 'learning', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subject_id) REFERENCES subjects(id) + )`); + // Pre-populate some subjects if empty db.get('SELECT COUNT(*) as count FROM subjects', (err, row) => { if (row && row.count === 0) { diff --git a/index.html b/index.html index 9e85926..337d273 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,41 @@ + StudyPlan + - + @@ -78,6 +107,10 @@

StudyPlan

Calendar + - -
+ +
April 2026
+ + + + + + + +
+ + +
+
+
+ + diff --git a/js/app.js b/js/app.js index 38a20f7..4c417d5 100644 --- a/js/app.js +++ b/js/app.js @@ -3,6 +3,7 @@ import { extractTasksFromText } from './utils/api.js'; import { initGlobalErrorBoundary } from './utils/errorBoundary.js'; import { analyzeWorkload } from './utils/scheduler.js'; import { Toast } from './utils/toast.js'; +import { initPlanner } from './planner.js'; initGlobalErrorBoundary(); @@ -1028,6 +1029,7 @@ store.subscribe(renderFocusTasks); store.subscribe(renderSidebarSubjects); document.addEventListener('DOMContentLoaded', () => { + initPlanner(); if (newSubjectColorsEl) { SUBJECT_COLORS.forEach(c => { const btn = document.createElement('button'); @@ -1381,7 +1383,7 @@ if (quoteEl) { quoteEl.textContent = quotes[index]; } -const calendarDownloadBtn = document.getElementById('calendar-download-btn'); +calendarDownloadBtn = document.getElementById('calendar-download-btn'); if (calendarDownloadBtn) { calendarDownloadBtn.addEventListener('click', () => { diff --git a/js/confetti.browser.min.js b/js/confetti.browser.min.js new file mode 100644 index 0000000..ca0ef96 --- /dev/null +++ b/js/confetti.browser.min.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using Terser v5.37.0. + * Original file: /npm/canvas-confetti@1.9.3/dist/confetti.browser.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(t,e){!function t(e,a,n,r){var o=!!(e.Worker&&e.Blob&&e.Promise&&e.OffscreenCanvas&&e.OffscreenCanvasRenderingContext2D&&e.HTMLCanvasElement&&e.HTMLCanvasElement.prototype.transferControlToOffscreen&&e.URL&&e.URL.createObjectURL),i="function"==typeof Path2D&&"function"==typeof DOMMatrix,l=function(){if(!e.OffscreenCanvas)return!1;var t=new OffscreenCanvas(1,1),a=t.getContext("2d");a.fillRect(0,0,1,1);var n=t.transferToImageBitmap();try{a.createPattern(n,"no-repeat")}catch(t){return!1}return!0}();function s(){}function c(t){var n=a.exports.Promise,r=void 0!==n?n:e.Promise;return"function"==typeof r?new r(t):(t(s,s),null)}var h,f,u,d,m,g,p,b,M,v,y,w=(h=l,f=new Map,{transform:function(t){if(h)return t;if(f.has(t))return f.get(t);var e=new OffscreenCanvas(t.width,t.height);return e.getContext("2d").drawImage(t,0,0),f.set(t,e),e},clear:function(){f.clear()}}),x=(m=Math.floor(1e3/60),g={},p=0,"function"==typeof requestAnimationFrame&&"function"==typeof cancelAnimationFrame?(u=function(t){var e=Math.random();return g[e]=requestAnimationFrame((function a(n){p===n||p+m-1 { + document.querySelectorAll('.sidebar .nav-item').forEach(el => el.classList.remove('active')); + plannerNavBtn.classList.add('active'); + if (tasksView) tasksView.style.display = 'none'; + if (plannerView) plannerView.style.display = 'flex'; + renderPlanner(); + }); + } + + // Hide planner view on other nav clicks + const otherNavs = ['calendar-btn', 'all-tasks-btn', 'archived-tasks-btn', 'focus-mode-btn']; + otherNavs.forEach(id => { + const btn = document.getElementById(id); + if (btn) { + btn.addEventListener('click', () => { + if (tasksView) tasksView.style.display = 'block'; + if (plannerView) plannerView.style.display = 'none'; + }); + } + }); + + // Init Modals + addExamBtn?.addEventListener('click', () => { + populateSubjectSelect('new-exam-subject'); + newExamModal.style.display = 'flex'; + }); + + addGoalBtn?.addEventListener('click', () => { + populateSubjectSelect('new-goal-subject'); + newGoalModal.style.display = 'flex'; + }); + + document.getElementById('new-exam-cancel')?.addEventListener('click', () => newExamModal.style.display = 'none'); + document.getElementById('new-goal-cancel')?.addEventListener('click', () => newGoalModal.style.display = 'none'); + + document.getElementById('new-exam-save')?.addEventListener('click', async () => { + const title = document.getElementById('new-exam-title').value; + const subject_id = document.getElementById('new-exam-subject').value; + const date = document.getElementById('new-exam-date').value; + if (await store.addExam({ title, subject_id, date })) { + newExamModal.style.display = 'none'; + } + }); + + document.getElementById('new-goal-save')?.addEventListener('click', async () => { + const description = document.getElementById('new-goal-desc').value; + const subject_id = document.getElementById('new-goal-subject').value; + const target_date = document.getElementById('new-goal-date').value; + if (await store.addGoal({ description, subject_id, target_date })) { + newGoalModal.style.display = 'none'; + } + }); + + generateScheduleBtn?.addEventListener('click', async () => { + const originalText = generateScheduleBtn.innerHTML; + generateScheduleBtn.innerHTML = '⏳ Generating...'; + generateScheduleBtn.disabled = true; + await store.generateSchedule(); + generateScheduleBtn.innerHTML = originalText; + generateScheduleBtn.disabled = false; + }); + + // Pomodoro Timer + let pomodoroInterval; + let timeLeft = 25 * 60; + let isRunning = false; + + const timeDisplay = document.getElementById('pomodoro-time'); + const startBtn = document.getElementById('pomodoro-start'); + const pauseBtn = document.getElementById('pomodoro-pause'); + const resetBtn = document.getElementById('pomodoro-reset'); + + function updateDisplay() { + const m = Math.floor(timeLeft / 60).toString().padStart(2, '0'); + const s = (timeLeft % 60).toString().padStart(2, '0'); + if (timeDisplay) timeDisplay.textContent = `${m}:${s}`; + } + + startBtn?.addEventListener('click', () => { + if (isRunning) return; + isRunning = true; + pomodoroInterval = setInterval(() => { + if (timeLeft > 0) { + timeLeft--; + updateDisplay(); + } else { + clearInterval(pomodoroInterval); + isRunning = false; + Toast.show('Pomodoro completed! Take a break.', 'success'); + } + }, 1000); + }); + + pauseBtn?.addEventListener('click', () => { + clearInterval(pomodoroInterval); + isRunning = false; + }); + + resetBtn?.addEventListener('click', () => { + clearInterval(pomodoroInterval); + isRunning = false; + timeLeft = 25 * 60; + updateDisplay(); + }); + + store.subscribe(renderPlanner); +} + +function populateSubjectSelect(selectId) { + const select = document.getElementById(selectId); + if (!select) return; + select.innerHTML = ''; + store.subjects.forEach(sub => { + const opt = document.createElement('option'); + opt.value = sub.id; + opt.textContent = sub.name; + select.appendChild(opt); + }); +} + +function renderPlanner() { + const plannerView = document.getElementById('planner-view'); + if (plannerView?.style.display === 'none') return; + + // Render Exams + const examList = document.getElementById('exam-list'); + if (examList) { + examList.innerHTML = store.exams.length ? store.exams.map(ex => { + const sub = store.subjects.find(s => s.id === ex.subject_id); + return `
+
${escapeHtml(ex.title)}
+
${escapeHtml(sub?.name || 'General')} • ${new Date(ex.date).toLocaleDateString()}
+
`; + }).join('') : '
No upcoming exams.
'; + } + + // Render Goals + const goalList = document.getElementById('goal-list'); + if (goalList) { + goalList.innerHTML = store.goals.length ? store.goals.map(g => { + const sub = store.subjects.find(s => s.id === g.subject_id); + return `
+
${escapeHtml(g.description)}
+
${escapeHtml(sub?.name || 'General')} • ${new Date(g.target_date).toLocaleDateString()}
+
`; + }).join('') : '
No active goals.
'; + } + + // Render Schedule + const timeline = document.getElementById('schedule-timeline'); + if (timeline) { + if (!store.studySessions || store.studySessions.length === 0) { + timeline.innerHTML = '
Click "Generate Smart Schedule" to let AI plan your studies.
'; + } else { + timeline.innerHTML = store.studySessions.map(s => { + const sub = store.subjects.find(sub => sub.id === s.subject_id); + const subColor = sub ? sub.color : 'var(--color-text-info)'; + const start = new Date(s.start_time); + const end = new Date(s.end_time); + const timeStr = `${start.toLocaleDateString()} ${start.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} - ${end.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}`; + + return `
+
+
${escapeHtml(s.title)}
+
+ ${escapeHtml(sub?.name || 'General')} + + ${timeStr} + ${s.type} +
+
+ +
`; + }).join(''); + } + } + + // Calc Streak + const streak = store.studySessions.filter(s => s.status === 'completed').length; + const streakEl = document.getElementById('streak-count'); + if (streakEl) streakEl.textContent = streak; +} + +window.toggleSession = (id) => { + store.toggleSessionStatus(id); +}; + +function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} diff --git a/js/store.js b/js/store.js index d561435..a1d6b1c 100644 --- a/js/store.js +++ b/js/store.js @@ -4,6 +4,9 @@ import { triggerConfetti } from './utils/confetti.js'; export const store = { subjects: [], tasks: [], + exams: [], + goals: [], + studySessions: [], currentPaste: null, listeners: [], @@ -25,12 +28,18 @@ export const store = { async fetchInitialData() { try { - const [subsRes, tasksRes] = await Promise.all([ + const [subsRes, tasksRes, examsRes, goalsRes, sessionsRes] = await Promise.all([ fetch('/api/subjects'), - fetch('/api/tasks') + fetch('/api/tasks'), + fetch('/api/exams'), + fetch('/api/study_goals'), + fetch('/api/study_sessions') ]); this.subjects = await subsRes.json(); this.tasks = await tasksRes.json(); + this.exams = await examsRes.json(); + this.goals = await goalsRes.json(); + this.studySessions = await sessionsRes.json(); this.notify(); } catch (e) { console.error('Failed to load initial data', e); @@ -317,5 +326,104 @@ export const store = { clearExtracted() { this.currentPaste = null; this.notify(); + }, + + // ================= PLANNER METHODS ================= + async addExam({ title, subject_id, date }) { + try { + const res = await fetch('/api/exams', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, subject_id, date }) + }); + if (!res.ok) throw new Error('Failed to add exam'); + const examsRes = await fetch('/api/exams'); + this.exams = await examsRes.json(); + this.notify(); + Toast.show('Exam added successfully', 'success'); + return true; + } catch (e) { + console.error(e); + Toast.show('Failed to add exam', 'error'); + return false; + } + }, + + async addGoal({ description, subject_id, target_date }) { + try { + const res = await fetch('/api/study_goals', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description, subject_id, target_date }) + }); + if (!res.ok) throw new Error('Failed to add goal'); + const goalsRes = await fetch('/api/study_goals'); + this.goals = await goalsRes.json(); + this.notify(); + Toast.show('Goal added successfully', 'success'); + return true; + } catch (e) { + console.error(e); + Toast.show('Failed to add goal', 'error'); + return false; + } + }, + + async generateSchedule() { + try { + // Determine weak subjects based on ratio of undone tasks (simple heuristic) + const weak_subjects = this.subjects.filter(s => { + const subTasks = this.tasks.filter(t => t.subject_id === s.id); + const undone = subTasks.filter(t => t.status !== 'Done'); + return undone.length > 2; // if more than 2 pending tasks, mark as weak + }).map(s => s.id); + + const res = await fetch('/api/planner/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tasks: this.tasks.filter(t => t.status !== 'Done').map(t => ({ title: t.title, due: t.due_at, subject: t.subject_id })), + exams: this.exams, + goals: this.goals, + weak_subjects + }) + }); + if (!res.ok) throw new Error('Failed to generate schedule'); + const sessionsRes = await fetch('/api/study_sessions'); + this.studySessions = await sessionsRes.json(); + this.notify(); + Toast.show('AI Schedule generated successfully!', 'success'); + triggerConfetti(); + return true; + } catch (e) { + console.error(e); + Toast.show('AI Generation failed. Check server logs.', 'error'); + return false; + } + }, + + async toggleSessionStatus(sessionId) { + const session = this.studySessions.find(s => s.id === sessionId); + if (!session) return; + + const newStatus = session.status === 'completed' ? 'pending' : 'completed'; + session.status = newStatus; + this.notify(); + + if (newStatus === 'completed') { + triggerConfetti(); + } + + try { + await fetch(`/api/study_sessions/${sessionId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }) + }); + } catch (e) { + session.status = newStatus === 'completed' ? 'pending' : 'completed'; + this.notify(); + console.error('Failed to update session status', e); + } } }; diff --git a/package-lock.json b/package-lock.json index 5652882..e9685a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "sqlite3": "^6.0.1" }, "engines": { - "node": "20.x" + "node": ">=20.x" } }, "node_modules/@google/genai": { diff --git a/server.js b/server.js index 47e1429..fa38d1f 100644 --- a/server.js +++ b/server.js @@ -252,8 +252,13 @@ function nlpExtractTasksFromText(text) { // ================= SUBJECTS ================= app.get('/api/subjects', (req, res) => { + console.log('GET /api/subjects route hit'); db.all('SELECT * FROM subjects', (err, rows) => { - if (err) return res.status(500).json({ error: err.message }); + if (err) { + console.error('GET /api/subjects database error:', err.message); + return res.status(500).json({ error: err.message }); + } + console.log(`GET /api/subjects returned ${rows.length} subjects`); res.json(rows); }); }); @@ -270,10 +275,13 @@ const ALLOWED_SUBJECT_COLORS = new Set([ app.post('/api/subjects', (req, res) => { const name = String(req.body?.name || '').trim(); let color = String(req.body?.color || '').trim() || 'var(--color-text-info)'; + console.log(`POST /api/subjects hit with name="${name}", color="${color}"`); if (!name) { + console.warn('POST /api/subjects: name is missing or empty'); return res.status(400).json({ error: 'Subject name is required' }); } if (!ALLOWED_SUBJECT_COLORS.has(color)) { + console.warn(`POST /api/subjects: color "${color}" is not allowed, falling back to default`); color = 'var(--color-text-info)'; } db.get( @@ -281,10 +289,12 @@ app.post('/api/subjects', (req, res) => { [name], (err, row) => { if (err) { + console.error('POST /api/subjects database check error:', err.message); return res.status(500).json({ error: err.message }); } if (row) { + console.warn(`POST /api/subjects: subject "${name}" already exists`); return res.status(400).json({ error: 'Subject already exists', }); @@ -296,7 +306,11 @@ app.post('/api/subjects', (req, res) => { 'INSERT INTO subjects (id, name, short_code, color) VALUES (?, ?, ?, ?)', [id, name, shortCode, color], function (err) { - if (err) return res.status(500).json({ error: err.message }); + if (err) { + console.error('POST /api/subjects insert database error:', err.message); + return res.status(500).json({ error: err.message }); + } + console.log(`POST /api/subjects successfully created subject "${name}" with ID=${id}`); res.status(201).json({ id, name, short_code: shortCode, color }); } ); @@ -473,6 +487,126 @@ app.delete('/api/tasks/:id', (req, res) => { ); }); +// ================= EXAMS ================= +app.get('/api/exams', (req, res) => { + db.all('SELECT * FROM exams ORDER BY date ASC', (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(rows); + }); +}); + +app.post('/api/exams', (req, res) => { + const { title, subject_id, date } = req.body; + if (!title || !subject_id || !date) return res.status(400).json({ error: 'Missing fields' }); + const id = 'exam_' + Date.now(); + db.run('INSERT INTO exams (id, title, subject_id, date) VALUES (?, ?, ?, ?)', [id, title, subject_id, date], function (err) { + if (err) return res.status(500).json({ error: err.message }); + res.status(201).json({ id, title, subject_id, date }); + }); +}); + +app.delete('/api/exams/:id', (req, res) => { + db.run('DELETE FROM exams WHERE id = ?', [req.params.id], function (err) { + if (err) return res.status(500).json({ error: err.message }); + res.json({ success: true, changes: this.changes }); + }); +}); + +// ================= STUDY GOALS ================= +app.get('/api/study_goals', (req, res) => { + db.all('SELECT * FROM study_goals ORDER BY target_date ASC', (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(rows); + }); +}); + +app.post('/api/study_goals', (req, res) => { + const { description, subject_id, target_date } = req.body; + if (!description || !subject_id) return res.status(400).json({ error: 'Missing fields' }); + const id = 'goal_' + Date.now(); + db.run('INSERT INTO study_goals (id, description, subject_id, target_date) VALUES (?, ?, ?, ?)', [id, description, subject_id, target_date], function (err) { + if (err) return res.status(500).json({ error: err.message }); + res.status(201).json({ id, description, subject_id, target_date }); + }); +}); + +app.delete('/api/study_goals/:id', (req, res) => { + db.run('DELETE FROM study_goals WHERE id = ?', [req.params.id], function (err) { + if (err) return res.status(500).json({ error: err.message }); + res.json({ success: true, changes: this.changes }); + }); +}); + +// ================= STUDY SESSIONS ================= +app.get('/api/study_sessions', (req, res) => { + db.all('SELECT * FROM study_sessions ORDER BY start_time ASC', (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(rows); + }); +}); + +app.put('/api/study_sessions/:id', (req, res) => { + const { status } = req.body; + db.run('UPDATE study_sessions SET status = ? WHERE id = ?', [status, req.params.id], function (err) { + if (err) return res.status(500).json({ error: err.message }); + res.json({ success: true, changes: this.changes }); + }); +}); + +app.post('/api/planner/generate', async (req, res) => { + const { tasks, exams, goals, weak_subjects } = req.body; + + if (ai) { + try { + const prompt = ` +You are an expert AI Study Planner. Create a 3-day study schedule based on the following: +Tasks: ${JSON.stringify(tasks)} +Exams: ${JSON.stringify(exams)} +Goals: ${JSON.stringify(goals)} +Weak Subjects (need more focus): ${JSON.stringify(weak_subjects)} + +Generate 2 to 4 study sessions per day. +Each session must have: +- title: string (what to do) +- start_time: ISO date string (assuming the first day is tomorrow at 10 AM, and sessions are 1 hour long) +- end_time: ISO date string +- subject_id: string (use one of the subject_ids from the data. If no subjects exist, use "sub_1") +- type: "learning" or "revision" + +Return ONLY a raw JSON array of session objects. +`; + const response = await ai.models.generateContent({ + model: 'gemini-2.5-flash', + contents: prompt + }); + + let rawText = (typeof response.text === 'function' ? response.text() : response.text).trim(); + if (rawText.startsWith('\`\`\`')) rawText = rawText.replace(/\`\`\`json|\`\`\`/g, '').trim(); + + const sessions = JSON.parse(rawText); + + // Save to database + const stmt = db.prepare('INSERT INTO study_sessions (id, title, start_time, end_time, subject_id, type) VALUES (?, ?, ?, ?, ?, ?)'); + const inserted = []; + for (const s of sessions) { + const id = 'sess_' + Date.now() + Math.random().toString(36).substr(2, 5); + s.id = id; + stmt.run([id, s.title, s.start_time, s.end_time, s.subject_id, s.type]); + inserted.push(s); + } + stmt.finalize(); + + return res.json({ success: true, sessions: inserted }); + + } catch (e) { + console.error('AI Schedule generation failed:', e.message); + return res.status(500).json({ error: 'Failed to generate schedule' }); + } + } else { + return res.status(500).json({ error: 'AI API key not configured' }); + } +}); + // ================= AI EXTRACTION ================= app.post('/api/extract', async (req, res) => { const { text } = req.body;