diff --git a/chat.html b/chat.html new file mode 100644 index 0000000..9459086 --- /dev/null +++ b/chat.html @@ -0,0 +1,99 @@ + + +
+ + +$1');
+ if (p.startsWith('- ')) p = '• ' + p.substring(2);
+ return `${p}
`; + }).join(''); + } + + function showTypingIndicator() { + const typingDiv = document.createElement('div'); + typingDiv.className = 'message bot-message typing-indicator'; + typingDiv.innerHTML = ''; + chatWindow.appendChild(typingDiv); + chatWindow.scrollTop = chatWindow.scrollHeight; + return typingDiv; + } + + function setupEventListeners() { + sendBtn.addEventListener('click', sendMessage); + chatInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); + chatInput.addEventListener('input', () => { chatInput.style.height = 'auto'; chatInput.style.height = `${chatInput.scrollHeight}px`; }); + subjectSelect.addEventListener('change', e => { currentSubject = e.target.value; }); + quickPrompts.forEach(btn => btn.addEventListener('click', () => { chatInput.value += ` ${btn.dataset.prompt}`; chatInput.focus(); })); + + + const clearChatBtn = document.getElementById('clear-chat-btn'); + if (clearChatBtn) { + clearChatBtn.addEventListener('click', async () => { + if (confirm("Are you sure you want to delete your entire chat history? This cannot be undone.")) { + try { + const res = await fetch(`${API_URL}/api/chat/history`, { + method: 'DELETE', + credentials: 'include' + }); + + if (res.ok) { + // Clear the chat window on the frontend + chatWindow.innerHTML = ''; + // Clear the history from local storage as well + localStorage.removeItem('chatHistory'); + chatHistory = []; + } else { + alert("Failed to clear chat history."); + } + } catch (error) { + console.error("Clear chat error:", error); + alert("Could not connect to the server to clear history."); + } + } + }); + } + + document.querySelector('.logout-link').addEventListener('click', async e => { + e.preventDefault(); + await fetch(`${API_URL}/logout`, { method: 'POST', credentials: 'include' }); + localStorage.clear(); + window.location.href = 'login.html'; + }); + } + + init(); +}); \ No newline at end of file diff --git a/dashboard.js b/dashboard.js new file mode 100644 index 0000000..fd114b5 --- /dev/null +++ b/dashboard.js @@ -0,0 +1,271 @@ +// File: dashboard.js (Complete and Final Code) +document.addEventListener('DOMContentLoaded', () => { + const API_URL = "http://localhost:5000"; + + // State variables + let userInfo = null, studentInfo = null, player = {}, subjects = [], quests = [], customSubjects = JSON.parse(localStorage.getItem('customSubjects')) || []; + + // DOM Elements + const levelDisplay = document.getElementById('player-level'), xpText = document.getElementById('xp-text'), xpBar = document.getElementById('xp-bar'); + const questTitleInput = document.getElementById('quest-title'), questSubjectSelect = document.getElementById('quest-subject'), questChapterInput = document.getElementById('quest-chapter'), questTopicInput = document.getElementById('quest-topic'), questDueDateInput = document.getElementById('quest-due-date'), questImportanceSelect = document.getElementById('quest-importance'), addQuestBtn = document.getElementById('add-quest-btn'), questListDiv = document.getElementById('quest-list'); + const profileButton = document.getElementById('profile-button'), profileModal = document.getElementById('profile-modal'), closeModalButton = document.querySelector('.close-button'), subjectListDiv = document.getElementById('subject-list'), newSubjectInput = document.getElementById('new-subject-input'), addSubjectModalBtn = document.getElementById('add-subject-modal-btn'); + const filterBtn = document.getElementById('filter-btn'), filterPanel = document.getElementById('filter-panel'), filterSubject = document.getElementById('filter-subject'), filterImportance = document.getElementById('filter-importance'), filterStatus = document.getElementById('filter-status'); + + async function init() { + try { + const meResponse = await fetch(`${API_URL}/api/me`, { credentials: 'include' }); + if (!meResponse.ok) { + window.location.href = 'login.html'; + return; + } + const meData = await meResponse.json(); + userInfo = meData.loggedInUser; + studentInfo = meData.studentInfo; + player = { level: studentInfo?.level || 1, xp: studentInfo?.total_xp || 0, xpToNextLevel: 100 * (studentInfo?.level || 1) }; + + displayStudentInfo(); + await loadSubjects(); + await loadQuests(); + updateUI(); + setupEventListeners(); + } catch (error) { + console.error("Initialization Error:", error); + window.location.href = 'login.html'; + } + } + + function displayStudentInfo() { + if (studentInfo) { + document.getElementById('student-name').textContent = studentInfo.name || userInfo.email.split('@')[0]; + document.getElementById('student-board').textContent = studentInfo.board + ' Board'; + document.getElementById('student-class').textContent = `Class ${studentInfo.class}`; + document.getElementById('profile-name').textContent = studentInfo.name || userInfo.email.split('@')[0]; + document.getElementById('profile-email').textContent = userInfo.email; + document.getElementById('profile-board').textContent = studentInfo.board; + document.getElementById('profile-class').textContent = studentInfo.class; + document.getElementById('profile-stream').textContent = studentInfo.stream || 'N/A'; + document.getElementById('profile-id').textContent = userInfo.student_id; + } + } + + async function loadSubjects() { + try { + const res = await fetch(`${API_URL}/api/subjects/${studentInfo?.class}/${studentInfo?.stream || ''}`); + if (res.ok) { + subjects = await res.json(); + subjects = [...subjects, ...customSubjects]; + populateSubjectsDropdown(); + populateFilterDropdown(); + } + } catch (err) { console.error('Error loading subjects:', err); } + } + + async function loadQuests() { + try { + const res = await fetch(`${API_URL}/quests`, { credentials: 'include' }); + if (res.ok) { + quests = await res.json(); + renderQuests(); + updateQuestStats(); + updateWeeklyStats(); + } + } catch (err) { console.error('Error loading quests:', err); } + } + + function updateUI() { + levelDisplay.textContent = `Level ${player.level}`; + const xpForCurrentLevel = player.xp - (100 * (player.level - 1)); + player.xpToNextLevel = 100 * player.level; + const xpPercentage = (xpForCurrentLevel / 100) * 100; + xpBar.style.width = `${xpPercentage}%`; + xpText.textContent = `${xpForCurrentLevel} / 100 XP`; + renderSubjectsInModal(); + } + + function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + document.body.appendChild(notification); + setTimeout(() => { + notification.classList.add('fade-out'); + setTimeout(() => notification.remove(), 300); + }, 2500); + } + + function showLevelUpNotification() { + const notification = document.createElement('div'); + notification.className = 'level-up-notification'; + notification.innerHTML = `You reached Level ${player.level}!
`; + document.body.appendChild(notification); + setTimeout(() => notification.remove(), 3000); + } + + function renderQuests() { + questListDiv.innerHTML = ''; + let filteredQuests = [...quests]; + if (filterSubject.value) filteredQuests = filteredQuests.filter(q => q.subject === filterSubject.value); + if (filterImportance.value) filteredQuests = filteredQuests.filter(q => q.importance === filterImportance.value); + if (filterStatus.value === 'pending') filteredQuests = filteredQuests.filter(q => !q.completed); + else if (filterStatus.value === 'completed') filteredQuests = filteredQuests.filter(q => q.completed); + + filteredQuests.sort((a, b) => { + if (!a.completed && b.completed) return -1; if (a.completed && !b.completed) return 1; + const dateA = new Date(a.due_date || '9999-12-31'), dateB = new Date(b.due_date || '9999-12-31'); + return dateA - dateB; + }); + + if (filteredQuests.length === 0) { + questListDiv.innerHTML = 'No quests found. Create one to begin your adventure!
'; + return; + } + filteredQuests.forEach(quest => questListDiv.appendChild(createQuestCard(quest))); + } + + function createQuestCard(quest) { + const dueDate = quest.due_date ? new Date(quest.due_date) : null; + const today = new Date(); today.setHours(0, 0, 0, 0); + let dueDateClass = '', dueDateText = 'No due date'; + if (dueDate) { + const daysUntilDue = Math.ceil((dueDate - today) / (1000 * 60 * 60 * 24)); + if (daysUntilDue < 0) { dueDateClass = 'overdue'; dueDateText = `Overdue by ${Math.abs(daysUntilDue)} days`; } + else if (daysUntilDue === 0) { dueDateClass = 'due-today'; dueDateText = 'Due today!'; } + else if (daysUntilDue <= 3) { dueDateClass = 'due-soon'; dueDateText = `Due in ${daysUntilDue} days`; } + else { dueDateText = dueDate.toLocaleDateString(); } + } + const questCard = document.createElement('div'); + questCard.className = `quest-card importance-${quest.importance} ${quest.completed ? 'completed' : ''}`; + questCard.innerHTML = ` +${quest.subject}
` : ''} + ${quest.chapter ? `Chapter: ${quest.chapter}
` : ''} + ${quest.topic ? `Topic: ${quest.topic}
` : ''} +${dueDateText}
+No custom subjects.
' : ''; + customSubjects.forEach((subject, index) => { + const item = document.createElement('div'); + item.className = 'subject-item'; + item.innerHTML = `${subject}`; + subjectListDiv.appendChild(item); + }); + document.querySelectorAll('.delete-subject-btn').forEach(btn => btn.addEventListener('click', e => deleteSubject(e.target.dataset.index))); + } + + function addSubjectFromModal() { + const newSubject = newSubjectInput.value.trim(); + if (newSubject && !subjects.includes(newSubject)) { + customSubjects.push(newSubject); + subjects.push(newSubject); + localStorage.setItem('customSubjects', JSON.stringify(customSubjects)); + newSubjectInput.value = ''; + populateSubjectsDropdown(); populateFilterDropdown(); renderSubjectsInModal(); + } else if (subjects.includes(newSubject)) alert('Subject already exists!'); + } + + function deleteSubject(index) { + const subject = customSubjects[index]; + if (confirm(`Delete "${subject}"?`)) { + customSubjects.splice(index, 1); + subjects = subjects.filter(s => s !== subject); + localStorage.setItem('customSubjects', JSON.stringify(customSubjects)); + populateSubjectsDropdown(); populateFilterDropdown(); renderSubjectsInModal(); + } + } + + function populateSubjectsDropdown() { + const options = subjects.map(s => ``).join(''); + questSubjectSelect.innerHTML = `${options}`; + } + + function populateFilterDropdown() { + const options = subjects.map(s => ``).join(''); + filterSubject.innerHTML = `${options}`; + } + + function setupEventListeners() { + addQuestBtn.addEventListener('click', addQuest); + profileButton.addEventListener('click', () => profileModal.style.display = 'flex'); + closeModalButton.addEventListener('click', () => profileModal.style.display = 'none'); + addSubjectModalBtn.addEventListener('click', addSubjectFromModal); + filterBtn.addEventListener('click', () => filterPanel.style.display = filterPanel.style.display === 'none' ? 'flex' : 'none'); + [filterSubject, filterImportance, filterStatus].forEach(el => el.addEventListener('change', renderQuests)); + document.querySelector('.logout-link').addEventListener('click', async e => { + e.preventDefault(); + await fetch(`${API_URL}/logout`, { method: 'POST', credentials: 'include' }); + localStorage.clear(); + window.location.href = 'login.html'; + }); + window.addEventListener('click', e => { if (e.target === profileModal) profileModal.style.display = 'none'; }); + } + + init(); +}); \ No newline at end of file diff --git a/hello.js b/hello.js new file mode 100644 index 0000000..1b88fb9 --- /dev/null +++ b/hello.js @@ -0,0 +1,487 @@ +import express from "express"; +import bodyParser from "body-parser"; +import { createClient } from "@supabase/supabase-js"; +import cors from "cors"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import dotenv from "dotenv"; +import bcrypt from "bcryptjs"; +import session from "express-session"; + +dotenv.config(); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY +); + +// Initialize Gemini AI +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + +const app = express(); + +// Middleware +app.use(cors({ + origin: ["http://localhost:3000", "http://127.0.0.1:5500"], + credentials: true +})); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.static('public')); +app.use(session({ + secret: process.env.SESSION_SECRET || 's3YwO7sf6y', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Indian Education Boards and Classes +const INDIAN_BOARDS = { + CBSE: "Central Board of Secondary Education", + ICSE: "Indian School Certificate Examinations", + MAHARASHTRA: "Maharashtra State Board", + UP: "Uttar Pradesh Board", + KARNATAKA: "Karnataka Board", + TAMIL_NADU: "Tamil Nadu Board", + WEST_BENGAL: "West Bengal Board" +}; + +const CLASSES = ["6", "7", "8", "9", "10", "11", "12"]; + +const SUBJECTS_BY_CLASS = { + "6-8": ["Mathematics", "Science", "Social Studies", "English", "Hindi", "Sanskrit"], + "9-10": ["Mathematics", "Science", "Social Science", "English", "Hindi", "Sanskrit", "Computer Science"], + "11-12-Science": ["Physics", "Chemistry", "Mathematics", "Biology", "Computer Science", "English"], + "11-12-Commerce": ["Accountancy", "Economics", "Business Studies", "Mathematics", "English"], + "11-12-Arts": ["History", "Political Science", "Geography", "Psychology", "Sociology", "English"] +}; + +// Check DB connection +async function checkDbConnection() { + try { + const { data, error } = await supabase.from("users").select("*").limit(1); + if (error) console.error("❌ Supabase connection failed:", error.message); + else console.log("✅ Supabase connection successful"); + } catch (err) { + console.error("❌ Error checking DB connection:", err.message); + } +} +checkDbConnection(); + +// ============= AUTH ROUTES ============= + +// Signup with password hashing +app.post("/signup", async (req, res) => { + try { + const { email, parent_email, password, studentClass, board, stream } = req.body; + + if (!email || !parent_email || !password || !studentClass || !board) { + return res.status(400).json({ error: "All fields are required" }); + } + + // Check if user exists + const { data: existingUser } = await supabase + .from("users") + .select("*") + .eq("email", email) + .single(); + + if (existingUser) { + return res.status(400).json({ error: "User already exists" }); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Generate student ID + const student_id = Math.floor(Math.random() * 900000) + 100000; + + // Insert into students table + const { error: studentError } = await supabase.from("students").insert([ + { + student_id, + class: studentClass, + board, + stream: stream || null, + name: email.split('@')[0], // Use email prefix as initial name + created_at: new Date().toISOString() + } + ]); + + if (studentError) { + return res.status(400).json({ error: studentError.message }); + } + + // Insert into users table + const { data, error } = await supabase.from("users").insert([ + { + student_id, + email, + parent_email, + password: hashedPassword, + created_at: new Date().toISOString() + } + ]); + + if (error) { + // Rollback student creation if user creation fails + await supabase.from("students").delete().eq("student_id", student_id); + return res.status(400).json({ error: error.message }); + } + + res.json({ + message: "User registered successfully", + student_id, + board: INDIAN_BOARDS[board] + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Login with password verification +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: "Email and password required" }); + } + + // Get user with student info + const { data: user, error } = await supabase + .from("users") + .select(` + *, + students ( + class, + board, + stream, + name + ) + `) + .eq("email", email) + .single(); + + if (error || !user) { + return res.status(400).json({ error: "Invalid credentials" }); + } + + // Verify password + const validPassword = await bcrypt.compare(password, user.password); + if (!validPassword) { + return res.status(400).json({ error: "Invalid credentials" }); + } + + // Create session + req.session.userId = user.id; + req.session.studentId = user.student_id; + req.session.userEmail = user.email; + req.session.studentInfo = user.students; + + // Don't send password to client + delete user.password; + + res.json({ + message: "Login successful", + data: user + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Logout +app.post("/logout", (req, res) => { + req.session.destroy((err) => { + if (err) { + return res.status(500).json({ error: "Could not log out" }); + } + res.json({ message: "Logged out successfully" }); + }); +}); + +// ============= GET CURRENT USER ============= +app.get("/api/me", async (req, res) => { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + try { + // Fetch fresh user and student data + const { data: user, error } = await supabase + .from("users") + .select(`*, students (*)`) + .eq("student_id", req.session.studentId) + .single(); + + if (error || !user) { + return res.status(404).json({ error: "User not found" }); + } + + // Don't send the password hash + delete user.password; + + // Send all necessary info to the frontend + res.json({ + loggedInUser: user, + studentInfo: user.students + }); + + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// ============= QUEST/TASK ROUTES ============= + +// Get user's quests +app.get("/quests", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { data, error } = await supabase + .from("quests") + .select("*") + .eq("student_id", req.session.studentId) + .order("created_at", { ascending: false }); + + if (error) { + return res.status(400).json({ error: error.message }); + } + + res.json(data); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Create quest +app.post("/quests", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { title, subject, dueDate, importance, chapter, topic } = req.body; + + const { data, error } = await supabase.from("quests").insert([ + { + student_id: req.session.studentId, + title, + subject, + due_date: dueDate, + importance, + chapter, + topic, + completed: false, + xp_value: importance === 'high' ? 50 : importance === 'medium' ? 25 : 10, + created_at: new Date().toISOString() + } + ]); + + if (error) { + return res.status(400).json({ error: error.message }); + } + + res.json({ message: "Quest created successfully", data }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Complete quest +app.put("/quests/:id/complete", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { id } = req.params; + + // Get quest to verify ownership and get XP value + const { data: quest, error: fetchError } = await supabase + .from("quests") + .select("*") + .eq("id", id) + .eq("student_id", req.session.studentId) + .single(); + + if (fetchError || !quest) { + return res.status(404).json({ error: "Quest not found" }); + } + + if (quest.completed) { + return res.status(400).json({ error: "Quest already completed" }); + } + + // Mark quest as completed + const { error: updateError } = await supabase + .from("quests") + .update({ + completed: true, + completed_at: new Date().toISOString() + }) + .eq("id", id); + + if (updateError) { + return res.status(400).json({ error: updateError.message }); + } + + // Update user's XP + const { data: student } = await supabase + .from("students") + .select("total_xp, level") + .eq("student_id", req.session.studentId) + .single(); + + const newXp = (student?.total_xp || 0) + quest.xp_value; + const newLevel = Math.floor(newXp / 100) + 1; + + await supabase + .from("students") + .update({ + total_xp: newXp, + level: newLevel + }) + .eq("student_id", req.session.studentId); + + res.json({ + message: "Quest completed!", + xp_earned: quest.xp_value, + total_xp: newXp, + level: newLevel + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// deleting chat history +app.delete("/api/chat/history", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { error } = await supabase + .from("chat_history") + .delete() + .eq("student_id", req.session.studentId); + + if (error) { + return res.status(500).json({ error: "Could not delete chat history." }); + } + + res.json({ message: "Chat history cleared successfully." }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// ============= AI CHAT ROUTES ============= + +// Enhanced AI chat with curriculum context +app.post("/api/chat", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { message, subject } = req.body; + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + + // Get student's curriculum info + const studentInfo = req.session.studentInfo || {}; + const contextPrompt = `You are Scholarly, an AI study buddy helping a ${studentInfo.board || 'Indian'} board Class ${studentInfo.class || ''} student. + ${studentInfo.stream ? `They are in the ${studentInfo.stream} stream.` : ''} + ${subject ? `The question is about ${subject}.` : ''} + Keep your answers relevant to their curriculum level and use terminology from Indian textbooks (especially NCERT when applicable). + Be encouraging, helpful, and concise.`; + + // Initialize Gemini model + const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash-latest" }); + + const chat = model.startChat({ + history: [ + { role: "user", parts: [{ text: contextPrompt }] }, + { role: "model", parts: [{ text: "Understood! I'm Scholarly, ready to help you with your studies! 📚" }] }, + ], + }); + + const result = await chat.sendMessage(message); + const response = result.response; + const text = response.text(); + + // Store chat history + await supabase.from("chat_history").insert([ + { + student_id: req.session.studentId, + message: message, + response: text, + subject: subject || null, + created_at: new Date().toISOString() + } + ]); + + res.json({ reply: text }); + + } catch (error) { + console.error("Gemini API Error:", error); + res.status(500).json({ error: "Failed to get a response from AI" }); + } +}); + +// ============= REFERENCE DATA ROUTES ============= + +// Get boards list +app.get("/api/boards", (req, res) => { + res.json(Object.keys(INDIAN_BOARDS).map(key => ({ + code: key, + name: INDIAN_BOARDS[key] + }))); +}); + +// Get classes list +app.get("/api/classes", (req, res) => { + res.json(CLASSES); +}); + +// Get subjects for a class/stream +app.get("/api/subjects/:class/:stream?", (req, res) => { + const { class: studentClass, stream } = req.params; + + let subjects = []; + if (["6", "7", "8"].includes(studentClass)) { + subjects = SUBJECTS_BY_CLASS["6-8"]; + } else if (["9", "10"].includes(studentClass)) { + subjects = SUBJECTS_BY_CLASS["9-10"]; + } else if (["11", "12"].includes(studentClass) && stream) { + subjects = SUBJECTS_BY_CLASS[`11-12-${stream}`] || []; + } + + res.json(subjects); +}); + +// Start server +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => { + console.log(`🚀 Server running at http://localhost:${PORT}`); + console.log(`📚 Scholarly - Gamified Learning Platform`); + console.log(`🎮 Ready to make studying fun!`); +}); \ No newline at end of file diff --git a/index.html b/index.html index 4310d96..ec1a2e7 100644 --- a/index.html +++ b/index.html @@ -1,89 +1,216 @@ - - - - - -Track your progress, complete quests, and get help from your AI study buddy. Your academic journey starts here.
+Official NCERT textbooks for CBSE curriculum - Free PDFs available
+Quality educational content aligned with Indian curriculum
+