const express = require('express'); const multer = require('multer'); const axios = require('axios'); const bodyParser = require('body-parser'); const cors = require('cors'); const session = require('express-session'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs-extra'); const path = require('path'); const bcrypt = require('bcrypt'); const flash = require('connect-flash'); require('dotenv').config(); // Document processing utilities const crypto = require('crypto'); // Document chunking configuration const CHUNK_SIZE = 20000; // Larger chunks for FormData uploads (20KB) const CHUNK_OVERLAP = 200; // Overlap between chunks const MAX_DOCUMENT_SIZE = 1000000; // 1MB limit for document content // Flowise document store configuration const FLOWISE_BASE_URL = 'https://flowise.suika.cc'; const FLOWISE_DOCUMENT_STORE_ID = 'e293dc23-7cb2-4522-a057-4f0c4b0a444c'; const FLOWISE_API_KEY = process.env.FLOWISE_API_KEY || null; const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static('public')); app.use(session({ secret: process.env.SESSION_SECRET || 'educat-secret-key', resave: false, saveUninitialized: false, cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24 hours })); app.use(flash()); // Set EJS as view engine app.set('view engine', 'ejs'); app.set('views', './views'); // Ensure uploads directory exists const uploadsDir = path.join(__dirname, 'uploads'); fs.ensureDirSync(uploadsDir); // Simple user storage (in production, use a proper database) const users = [ { id: 1, username: 'admin', email: 'admin@educat.com', password: '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password name: 'Admin User' }, { id: 2, username: 'student', email: 'student@educat.com', password: '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password name: 'Student User' } ]; // Initialize demo users with proper password hashing async function initializeDemoUsers() { try { const hashedPassword = await bcrypt.hash('password', 10); users[0].password = hashedPassword; users[1].password = hashedPassword; } catch (error) { console.error('Error initializing demo users:', error); } } // Initialize demo users on startup initializeDemoUsers(); // Authentication middleware const requireAuth = (req, res, next) => { if (req.session.userId) { next(); } else { req.flash('error', 'Please log in to access this page'); res.redirect('/login'); } }; // Add user info to all templates app.use((req, res, next) => { res.locals.user = req.session.userId ? users.find(u => u.id === req.session.userId) : null; res.locals.messages = req.flash(); next(); }); // Configure multer for file uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 // 10MB limit }, fileFilter: (req, file, cb) => { // Allow text files, PDFs, and images const allowedTypes = /jpeg|jpg|png|gif|pdf|txt|doc|docx/; const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = allowedTypes.test(file.mimetype); if (mimetype && extname) { return cb(null, true); } else { cb(new Error('Only text files, PDFs, and images are allowed!')); } } }); // Flowise API configuration const FLOWISE_API_URL = process.env.FLOWISE_API_URL || 'https://flowise.suika.cc/api/v1/prediction'; const FLOWISE_CHATFLOW_ID = process.env.FLOWISE_CHATFLOW_ID || 'your-chatflow-id'; // Helper function to chunk text into smaller pieces function chunkText(text, chunkSize = CHUNK_SIZE, overlap = CHUNK_OVERLAP) { const chunks = []; let start = 0; while (start < text.length) { const end = Math.min(start + chunkSize, text.length); const chunk = text.substring(start, end); // Only add non-empty chunks if (chunk.trim().length > 0) { chunks.push({ content: chunk.trim(), metadata: { chunkIndex: chunks.length, startIndex: start, endIndex: end, totalLength: text.length } }); } // Move start position, accounting for overlap start = end - overlap; // Break if we're at the end if (end >= text.length) break; } return chunks; } // Helper function to clean and validate document content function validateAndCleanDocument(content, originalName) { // Remove excessive whitespace and normalize line endings let cleanContent = content .replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\n{3,}/g, '\n\n') .replace(/[ \t]{2,}/g, ' ') .trim(); // Check document size if (cleanContent.length > MAX_DOCUMENT_SIZE) { throw new Error(`Document is too large (${cleanContent.length} characters). Maximum size is ${MAX_DOCUMENT_SIZE} characters.`); } // Check if document has meaningful content if (cleanContent.length < 50) { throw new Error('Document content is too short. Please upload a document with at least 50 characters.'); } // Remove or replace problematic characters that might cause issues cleanContent = cleanContent .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters .replace(/[^\x20-\x7E\n\t]/g, (char) => { // Replace non-printable characters const code = char.charCodeAt(0); return code > 127 ? char : ''; // Keep Unicode characters, remove other non-printable }); return cleanContent; } // Helper function to generate document hash for deduplication function generateDocumentHash(content, originalName) { return crypto.createHash('sha256') .update(content + originalName) .digest('hex') .substring(0, 16); } // Helper function to get document loaders from document store async function getDocumentStoreLoaders(documentStoreId) { try { console.log(`Getting document store loaders for: ${documentStoreId}`); const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; if (FLOWISE_API_KEY) { headers['Authorization'] = `Bearer ${FLOWISE_API_KEY}`; } // Get document store details which includes loaders const storeUrl = `${FLOWISE_BASE_URL}/api/v1/document-store/store/${documentStoreId}`; const response = await axios.get(storeUrl, { headers, timeout: 10000 }); console.log('Document store details:', { status: response.status, id: response.data.id, name: response.data.name, loaders: response.data.loaders }); // Parse loaders - they may come as array directly or as JSON string let loaders = []; if (response.data.loaders) { if (Array.isArray(response.data.loaders)) { // Loaders are already an array loaders = response.data.loaders; console.log('Loaders received as array:', loaders.length, 'loaders found'); } else if (typeof response.data.loaders === 'string') { // Loaders are a JSON string that needs parsing try { loaders = JSON.parse(response.data.loaders); console.log('Loaders parsed from JSON string:', loaders.length, 'loaders found'); } catch (parseError) { console.log('Could not parse loaders JSON string, will create new one'); loaders = []; } } else { console.log('Loaders in unexpected format, will create new one'); loaders = []; } } return { store: response.data, loaders: loaders }; } catch (error) { console.error('Error getting document store loaders:', { error: error.message, response: error.response ? { status: error.response.status, data: error.response.data } : 'No response' }); return null; } } // Helper function to upsert document to Flowise using FormData (direct file upload) async function upsertDocumentToFlowiseFormData(fileInfo, documentMetadata) { console.log('Starting Flowise Document Store upsert with FormData...'); console.log(`Document: ${documentMetadata.originalName}`); console.log(`Original file path: ${fileInfo.path}`); console.log(`Document Store ID: ${FLOWISE_DOCUMENT_STORE_ID}`); try { // Create FormData for file upload const FormData = require('form-data'); const formData = new FormData(); // Read the file and append to FormData const filePath = path.isAbsolute(fileInfo.path) ? fileInfo.path : path.join(__dirname, fileInfo.path); const fileStream = fs.createReadStream(filePath); // Try to get existing document store info to find existing loaders const storeInfo = await getDocumentStoreLoaders(FLOWISE_DOCUMENT_STORE_ID); let docId = null; let useExistingLoader = false; if (storeInfo && storeInfo.loaders && storeInfo.loaders.length > 0) { // Use the first existing loader ID, but only if replaceExisting is true docId = storeInfo.loaders[0].id || storeInfo.loaders[0].loaderId; useExistingLoader = true; console.log(`Using existing document loader ID: ${docId}`); console.log(`Existing loader details:`, { id: docId, loaderName: storeInfo.loaders[0].loaderName, splitterName: storeInfo.loaders[0].splitterName, totalChunks: storeInfo.loaders[0].totalChunks, status: storeInfo.loaders[0].status }); } else { // Create a new loader by letting Flowise auto-generate or create new store console.log('No existing loaders found, will create new document entry'); } // Append form fields following the Flowise API structure formData.append('files', fileStream, { filename: fileInfo.originalName, contentType: fileInfo.mimetype }); // Only append docId if we have an existing loader, otherwise let Flowise create new if (useExistingLoader && docId) { formData.append('docId', docId); formData.append('replaceExisting', 'true'); } else { // For new documents, don't specify docId and create new store entry formData.append('replaceExisting', 'false'); formData.append('createNewDocStore', 'true'); } formData.append('splitter', JSON.stringify({ "config": { "chunkSize": CHUNK_SIZE, "chunkOverlap": CHUNK_OVERLAP } })); // Add metadata const metadata = { documentId: documentMetadata.documentId, originalName: documentMetadata.originalName, uploadDate: documentMetadata.uploadDate, userId: documentMetadata.userId, source: 'EduCat', fileSize: fileInfo.size, mimetype: fileInfo.mimetype }; formData.append('metadata', JSON.stringify(metadata)); // Don't duplicate the replaceExisting and createNewDocStore - they're set above // Prepare the upsert URL const upsertUrl = `${FLOWISE_BASE_URL}/api/v1/document-store/upsert/${FLOWISE_DOCUMENT_STORE_ID}`; console.log(`Upserting to: ${upsertUrl}`); // Prepare headers const headers = { ...formData.getHeaders() }; if (FLOWISE_API_KEY) { headers['Authorization'] = `Bearer ${FLOWISE_API_KEY}`; } console.log('Sending FormData upsert request...'); console.log('FormData fields:', { docId: docId || 'auto-generated', fileName: fileInfo.originalName, fileSize: fileInfo.size, replaceExisting: useExistingLoader, createNewDocStore: !useExistingLoader, chunkSize: CHUNK_SIZE, useExistingLoader: useExistingLoader }); // Make the request using axios with FormData const response = await axios.post(upsertUrl, formData, { headers, timeout: 120000, // 2 minute timeout for large files maxContentLength: Infinity, maxBodyLength: Infinity }); console.log('FormData upsert successful:', { status: response.status, statusText: response.statusText, data: response.data }); return { success: true, totalChunks: 1, // FormData uploads are treated as single operations successfulChunks: 1, failedChunks: 0, results: [{ chunkIndex: 0, success: true, response: response.data }], errors: [] }; } catch (error) { console.error('Flowise FormData upsert failed:', { error: error.message, response: error.response ? { status: error.response.status, statusText: error.response.statusText, data: error.response.data } : 'No response', config: error.config ? { url: error.config.url, method: error.config.method } : 'No config' }); // Fall back to local storage if Flowise fails console.log('Falling back to local storage...'); try { const documentData = { id: documentMetadata.documentId, fileInfo: fileInfo, metadata: documentMetadata, storedAt: new Date().toISOString(), fallbackReason: 'flowise_formdata_upsert_failed' }; const documentsDir = path.join(__dirname, 'data', 'documents'); await fs.ensureDir(documentsDir); const documentPath = path.join(documentsDir, `${documentMetadata.documentId}.json`); await fs.writeJSON(documentPath, documentData, { spaces: 2 }); console.log(`Document stored locally as fallback: ${documentPath}`); return { success: true, totalChunks: 1, successfulChunks: 1, failedChunks: 0, results: [{ chunkIndex: 0, success: true, response: { status: 'stored_locally', message: 'Fallback to local storage' } }], errors: [] }; } catch (fallbackError) { console.error('Local storage fallback also failed:', fallbackError); return { success: false, totalChunks: 1, successfulChunks: 0, failedChunks: 1, results: [], errors: [{ chunkIndex: 0, error: `Flowise FormData upsert failed: ${error.message}. Local fallback failed: ${fallbackError.message}`, chunk: 'Complete document' }] }; } } } // Routes app.get('/', (req, res) => { res.render('index', { title: 'EduCat - AI-Powered Note Revision', messages: req.flash() }); }); // Authentication routes app.get('/login', (req, res) => { if (req.session.userId) { return res.redirect('/dashboard'); } res.render('login', { title: 'Login - EduCat', messages: req.flash() }); }); app.post('/login', async (req, res) => { try { const { username, password } = req.body; if (!username || !password) { req.flash('error', 'Please provide both username and password'); return res.redirect('/login'); } const user = users.find(u => u.username === username || u.email === username); if (!user) { req.flash('error', 'Invalid username or password'); return res.redirect('/login'); } const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { req.flash('error', 'Invalid username or password'); return res.redirect('/login'); } req.session.userId = user.id; req.flash('success', `Welcome back, ${user.name}!`); res.redirect('/dashboard'); } catch (error) { console.error('Login error:', error); req.flash('error', 'An error occurred during login'); res.redirect('/login'); } }); app.get('/register', (req, res) => { if (req.session.userId) { return res.redirect('/dashboard'); } res.render('register', { title: 'Register - EduCat', messages: req.flash() }); }); app.post('/register', async (req, res) => { try { const { username, email, password, confirmPassword, name } = req.body; if (!username || !email || !password || !confirmPassword || !name) { req.flash('error', 'Please fill in all fields'); return res.redirect('/register'); } if (password !== confirmPassword) { req.flash('error', 'Passwords do not match'); return res.redirect('/register'); } if (password.length < 6) { req.flash('error', 'Password must be at least 6 characters long'); return res.redirect('/register'); } // Check if user already exists const existingUser = users.find(u => u.username === username || u.email === email); if (existingUser) { req.flash('error', 'Username or email already exists'); return res.redirect('/register'); } // Hash password const hashedPassword = await bcrypt.hash(password, 10); // Create new user const newUser = { id: users.length + 1, username, email, password: hashedPassword, name }; users.push(newUser); req.session.userId = newUser.id; req.flash('success', `Welcome to EduCat, ${newUser.name}!`); res.redirect('/dashboard'); } catch (error) { console.error('Registration error:', error); req.flash('error', 'An error occurred during registration'); res.redirect('/register'); } }); app.get('/logout', (req, res) => { req.session.destroy((err) => { if (err) { console.error('Logout error:', err); } res.redirect('/'); }); }); app.get('/upload', requireAuth, (req, res) => { res.render('upload', { title: 'Upload Your Notes - EduCat' }); }); app.post('/upload', requireAuth, upload.single('noteFile'), async (req, res) => { try { console.log('=== UPLOAD REQUEST START ==='); console.log('Request file:', req.file); console.log('Request body:', req.body); console.log('Session userId:', req.session.userId); if (!req.file) { console.log('ERROR: No file uploaded'); return res.status(400).json({ success: false, error: 'No file uploaded' }); } const fileInfo = { id: uuidv4(), originalName: req.file.originalname, filename: req.file.filename, path: req.file.path, size: req.file.size, mimetype: req.file.mimetype, uploadDate: new Date().toISOString(), userId: req.session.userId, status: 'processing' }; console.log('Created fileInfo:', fileInfo); // Store initial file info in session if (!req.session.uploadedFiles) { req.session.uploadedFiles = []; } req.session.uploadedFiles.push(fileInfo); console.log('Total uploaded files in session:', req.session.uploadedFiles.length); // Send immediate response to prevent timeout res.json({ success: true, message: 'File uploaded successfully! Processing document...', fileInfo: fileInfo, processing: true }); console.log('Response sent, starting async processing...'); // Process document asynchronously processDocumentAsync(fileInfo, req.session.userId); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload failed', details: error.message }); } }); // Async function to process document and upsert to Flowise using FormData async function processDocumentAsync(fileInfo, userId) { try { console.log('=== DOCUMENT PROCESSING START (FormData) ==='); console.log(`Starting document processing for: ${fileInfo.originalName}`); console.log(`File path: ${fileInfo.path}`); console.log(`File size: ${fileInfo.size} bytes`); console.log(`User ID: ${userId}`); // Create document metadata const documentMetadata = { documentId: fileInfo.id, originalName: fileInfo.originalName, uploadDate: fileInfo.uploadDate, userId: userId, fileSize: fileInfo.size, fileInfo: fileInfo // Pass file info for progress tracking }; console.log('Document metadata:', documentMetadata); // Initialize processing result fileInfo.processingResult = { totalChunks: 1, // FormData uploads are single operations successfulChunks: 0, failedChunks: 0, startedAt: new Date().toISOString(), status: 'uploading_to_flowise' }; console.log('About to start FormData upsert to Flowise...'); // Upsert document to Flowise using FormData const upsertResult = await upsertDocumentToFlowiseFormData(fileInfo, documentMetadata); console.log('FormData upsert result:', upsertResult); // Update file info with processing results fileInfo.status = upsertResult.success ? 'processed' : 'failed'; fileInfo.processingProgress = null; // Clear progress when done fileInfo.processingResult = { totalChunks: upsertResult.totalChunks, successfulChunks: upsertResult.successfulChunks, failedChunks: upsertResult.failedChunks, processedAt: new Date().toISOString(), startedAt: fileInfo.processingResult.startedAt, duration: Date.now() - new Date(fileInfo.processingResult.startedAt).getTime(), method: 'formdata' }; if (upsertResult.errors.length > 0) { fileInfo.processingErrors = upsertResult.errors; console.log(`Processing errors for ${fileInfo.originalName}:`, upsertResult.errors); } console.log(`Document processing completed for: ${fileInfo.originalName}`); console.log(`Result: FormData upload ${upsertResult.success ? 'successful' : 'failed'}`); // Log final upsert verification console.log('Final FormData upsert verification:', { documentStore: `${FLOWISE_BASE_URL}/api/v1/document-store/${FLOWISE_DOCUMENT_STORE_ID}`, documentId: fileInfo.id, fileName: fileInfo.originalName, success: upsertResult.success }); } catch (error) { console.error(`Error processing document ${fileInfo.originalName}:`, error); fileInfo.status = 'failed'; fileInfo.processingError = error.message; fileInfo.processingResult = fileInfo.processingResult || {}; fileInfo.processingResult.failedAt = new Date().toISOString(); } } app.get('/revise/:fileId', requireAuth, async (req, res) => { try { const fileId = req.params.fileId; const files = req.session.uploadedFiles || []; const file = files.find(f => f.id === fileId); if (!file) { return res.status(404).render('error', { title: 'File Not Found', error: 'File not found' }); } // Read file content const fileContent = await fs.readFile(file.path, 'utf-8'); res.render('revise', { title: 'Revise Notes - EduCat', file: file, content: fileContent }); } catch (error) { console.error('Error loading file:', error); res.status(500).render('error', { title: 'Error', error: 'Failed to load file' }); } }); app.post('/api/revise', requireAuth, async (req, res) => { try { const { content, revisionType, fileId } = req.body; let prompt = ''; let contextContent = content; // If fileId is provided, try to get additional context from stored document if (fileId) { try { const documentPath = path.join(__dirname, 'data', 'documents', `${fileId}.json`); if (await fs.pathExists(documentPath)) { const documentData = await fs.readJSON(documentPath); contextContent = documentData.content; console.log(`Using stored document content for revision: ${documentData.metadata.originalName}`); } } catch (error) { console.log('Could not load stored document, using provided content'); } } switch (revisionType) { case 'summarize': prompt = `Please summarize the following notes in a clear and concise manner:\n\n${contextContent}`; break; case 'improve': prompt = `Please improve and enhance the following notes, making them more comprehensive and well-structured:\n\n${contextContent}`; break; case 'questions': prompt = `Based on the following notes, generate study questions that would help test understanding:\n\n${contextContent}`; break; default: prompt = `Please help improve these notes:\n\n${contextContent}`; } // Call Flowise API const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { question: prompt, history: [] }); res.json({ success: true, revisedContent: response.data.text || response.data.answer || 'No response received' }); } catch (error) { console.error('Revision error:', error); res.status(500).json({ error: 'Failed to revise notes', details: error.message }); } }); app.get('/chat', requireAuth, (req, res) => { res.render('chat', { title: 'Chat with EduCat AI' }); }); app.post('/api/chat', requireAuth, async (req, res) => { try { const { message, history } = req.body; // Call Flowise API for chat const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { question: message, history: history || [] }); res.json({ success: true, response: response.data.text || response.data.answer || 'No response received' }); } catch (error) { console.error('Chat error:', error); res.status(500).json({ error: 'Failed to get chat response', details: error.message }); } }); app.get('/dashboard', requireAuth, (req, res) => { const files = req.session.uploadedFiles || []; res.render('dashboard', { title: 'Dashboard - EduCat', files: files }); }); // File management endpoints app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => { try { const fileId = req.params.fileId; const files = req.session.uploadedFiles || []; const file = files.find(f => f.id === fileId); if (!file) { return res.status(404).json({ success: false, error: 'File not found' }); } // Read file content const filePath = path.join(__dirname, file.path); const fileContent = await fs.readFile(filePath, 'utf-8'); res.json({ success: true, file: { id: file.id, originalName: file.originalName, size: file.size, uploadDate: file.uploadDate, content: fileContent } }); } catch (error) { console.error('Error previewing file:', error); res.status(500).json({ success: false, error: 'Failed to preview file', details: error.message }); } }); app.delete('/api/files/:fileId', requireAuth, async (req, res) => { try { const fileId = req.params.fileId; const files = req.session.uploadedFiles || []; const fileIndex = files.findIndex(f => f.id === fileId); if (fileIndex === -1) { return res.status(404).json({ success: false, error: 'File not found' }); } const file = files[fileIndex]; // Delete the physical file const filePath = path.join(__dirname, file.path); if (await fs.pathExists(filePath)) { await fs.unlink(filePath); } // Remove from session req.session.uploadedFiles.splice(fileIndex, 1); console.log(`File deleted: ${file.originalName} (ID: ${fileId})`); res.json({ success: true, message: 'File deleted successfully' }); } catch (error) { console.error('Error deleting file:', error); res.status(500).json({ success: false, error: 'Failed to delete file', details: error.message }); } }); // File processing status endpoint app.get('/api/files/:fileId/status', requireAuth, async (req, res) => { try { const fileId = req.params.fileId; const files = req.session.uploadedFiles || []; const file = files.find(f => f.id === fileId); if (!file) { return res.status(404).json({ success: false, error: 'File not found' }); } res.json({ success: true, file: { id: file.id, originalName: file.originalName, status: file.status, uploadDate: file.uploadDate, processingResult: file.processingResult, processingError: file.processingError, processingErrors: file.processingErrors } }); } catch (error) { console.error('Error getting file status:', error); res.status(500).json({ success: false, error: 'Failed to get file status', details: error.message }); } }); // Retry processing endpoint app.post('/api/files/:fileId/retry', requireAuth, async (req, res) => { try { const fileId = req.params.fileId; const files = req.session.uploadedFiles || []; const file = files.find(f => f.id === fileId); if (!file) { return res.status(404).json({ success: false, error: 'File not found' }); } if (file.status === 'processing') { return res.status(400).json({ success: false, error: 'File is already being processed' }); } // Reset file status for retry file.status = 'processing'; file.processingResult = null; file.processingError = null; file.processingErrors = null; // Start processing asynchronously processDocumentAsync(file, req.session.userId); res.json({ success: true, message: 'Processing retry initiated' }); } catch (error) { console.error('Error retrying processing:', error); res.status(500).json({ success: false, error: 'Failed to retry processing', details: error.message }); } }); // Bulk processing status endpoint app.get('/api/files/status/all', requireAuth, async (req, res) => { try { const files = req.session.uploadedFiles || []; const statusSummary = files.map(file => ({ id: file.id, originalName: file.originalName, status: file.status, uploadDate: file.uploadDate, processingResult: file.processingResult ? { totalChunks: file.processingResult.totalChunks, successfulChunks: file.processingResult.successfulChunks, failedChunks: file.processingResult.failedChunks, processedAt: file.processingResult.processedAt } : null })); const summary = { totalFiles: files.length, processing: files.filter(f => f.status === 'processing').length, processed: files.filter(f => f.status === 'processed').length, failed: files.filter(f => f.status === 'failed').length, files: statusSummary }; res.json({ success: true, summary: summary }); } catch (error) { console.error('Error getting files status:', error); res.status(500).json({ success: false, error: 'Failed to get files status', details: error.message }); } }); app.get('/quiz', requireAuth, (req, res) => { res.render('quiz', { title: 'AI Quiz Generator - EduCat' }); }); app.post('/api/generate-quiz', requireAuth, async (req, res) => { console.log('=== QUIZ API REQUEST RECEIVED ==='); console.log('Request body:', req.body); console.log('User ID:', req.session.userId); console.log('================================='); try { const { topic, difficulty, questionCount, quizType } = req.body; console.log('Quiz request:', { topic, difficulty, questionCount, quizType }); let prompt = ''; switch (quizType) { case 'multiple-choice': prompt = `Generate exactly ${questionCount} multiple choice questions about "${topic}" at ${difficulty} difficulty level. Each question should have 4 options (A, B, C, D). Format the response as a JSON array where each question follows this exact structure: [ { "question": "What is the capital of France?", "options": ["A) London", "B) Paris", "C) Berlin", "D) Madrid"], "correct": "B", "explanation": "Paris is the capital and largest city of France." } ] Return ONLY the JSON array, no additional text.`; break; case 'true-false': prompt = `Generate exactly ${questionCount} true/false questions about "${topic}" at ${difficulty} difficulty level. Format the response as a JSON array where each question follows this exact structure: [ { "question": "The Earth is flat.", "correct": "False", "explanation": "The Earth is spherical, not flat." } ] Return ONLY the JSON array, no additional text.`; break; case 'short-answer': prompt = `Generate exactly ${questionCount} short answer questions about "${topic}" at ${difficulty} difficulty level. Format the response as a JSON array where each question follows this exact structure: [ { "question": "What is the process by which plants make their own food?", "answer": "Photosynthesis", "keywords": ["photosynthesis", "sunlight", "chlorophyll"] } ] Return ONLY the JSON array, no additional text.`; break; default: prompt = `Generate exactly ${questionCount} multiple choice questions about "${topic}" at ${difficulty} difficulty level.`; } console.log('Sending prompt to Flowise:', prompt.substring(0, 100) + '...'); // Call Flowise API const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { question: prompt, history: [] }); console.log('Flowise response received:', { status: response.status, dataType: typeof response.data, dataPreview: JSON.stringify(response.data).substring(0, 200) + '...' }); let quizData; try { const responseText = response.data.text || response.data.answer || response.data; console.log('Raw response text preview:', responseText.substring(0, 500) + '...'); // Try to extract JSON from the response let jsonString = null; // First, try to find JSON wrapped in code blocks const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/); if (codeBlockMatch) { jsonString = codeBlockMatch[1]; console.log('Found JSON in code block'); } else { // Try to find JSON array by counting brackets const startIndex = responseText.indexOf('['); if (startIndex !== -1) { let bracketCount = 0; let endIndex = startIndex; for (let i = startIndex; i < responseText.length; i++) { if (responseText[i] === '[') bracketCount++; if (responseText[i] === ']') bracketCount--; if (bracketCount === 0) { endIndex = i; break; } } if (bracketCount === 0) { jsonString = responseText.substring(startIndex, endIndex + 1); console.log('Found JSON by bracket counting'); } } } if (jsonString) { console.log('Parsing JSON string:', jsonString.substring(0, 200) + '...'); quizData = JSON.parse(jsonString); console.log('Successfully parsed quiz data, questions:', quizData.length); } else { console.log('No JSON found, using fallback quiz'); quizData = generateFallbackQuiz(topic, questionCount, quizType); } } catch (parseError) { console.error('Quiz parsing error:', parseError); console.log('Using fallback quiz due to parsing error'); quizData = generateFallbackQuiz(topic, questionCount, quizType); } console.log('Final quiz data:', { questionsCount: quizData.length, firstQuestion: quizData[0] }); res.json({ success: true, quiz: quizData, topic: topic, difficulty: difficulty, questionCount: questionCount, quizType: quizType }); } catch (error) { console.error('Quiz generation error:', error); console.error('Error stack:', error.stack); // Return fallback quiz on error const fallbackQuiz = generateFallbackQuiz(req.body.topic || 'General Knowledge', req.body.questionCount || 5, req.body.quizType || 'multiple-choice'); console.log('Returning fallback quiz due to error'); res.json({ success: true, quiz: fallbackQuiz, topic: req.body.topic || 'General Knowledge', difficulty: req.body.difficulty || 'beginner', questionCount: req.body.questionCount || 5, quizType: req.body.quizType || 'multiple-choice' }); } }); function generateFallbackQuiz(topic, questionCount, quizType) { const questions = []; // Generate actual questions based on topic const topicQuestions = { 'javascript': [ { question: 'What is the correct way to declare a variable in JavaScript?', options: ['A) var myVar = 5;', 'B) variable myVar = 5;', 'C) declare myVar = 5;', 'D) int myVar = 5;'], correct: 'A', explanation: 'The var keyword is used to declare variables in JavaScript.' }, { question: 'Which method is used to add an element to the end of an array?', options: ['A) push()', 'B) add()', 'C) append()', 'D) insert()'], correct: 'A', explanation: 'The push() method adds one or more elements to the end of an array.' }, { question: 'What does === operator do in JavaScript?', options: ['A) Assigns a value', 'B) Compares values only', 'C) Compares values and types', 'D) Declares a constant'], correct: 'C', explanation: 'The === operator compares both value and type without type conversion.' }, { question: 'How do you write a comment in JavaScript?', options: ['A) ', 'B) // comment', 'C) # comment', 'D) /* comment */'], correct: 'B', explanation: 'Single-line comments in JavaScript start with //.' }, { question: 'What is the result of typeof null in JavaScript?', options: ['A) "null"', 'B) "undefined"', 'C) "object"', 'D) "boolean"'], correct: 'C', explanation: 'typeof null returns "object" due to a legacy bug in JavaScript.' } ], 'python': [ { question: 'Which keyword is used to create a function in Python?', options: ['A) function', 'B) def', 'C) create', 'D) func'], correct: 'B', explanation: 'The def keyword is used to define functions in Python.' }, { question: 'What is the correct way to create a list in Python?', options: ['A) list = (1, 2, 3)', 'B) list = {1, 2, 3}', 'C) list = [1, 2, 3]', 'D) list = <1, 2, 3>'], correct: 'C', explanation: 'Square brackets [] are used to create lists in Python.' }, { question: 'How do you start a comment in Python?', options: ['A) //', 'B) #', 'C) /*', 'D) --'], correct: 'B', explanation: 'Comments in Python start with the # symbol.' }, { question: 'What does len() function do in Python?', options: ['A) Returns the length of an object', 'B) Converts to lowercase', 'C) Rounds a number', 'D) Prints output'], correct: 'A', explanation: 'The len() function returns the number of items in an object.' }, { question: 'Which of the following is a mutable data type in Python?', options: ['A) tuple', 'B) string', 'C) list', 'D) integer'], correct: 'C', explanation: 'Lists are mutable, meaning they can be changed after creation.' } ], 'math': [ { question: 'What is the value of π (pi) approximately?', options: ['A) 3.14159', 'B) 2.71828', 'C) 1.61803', 'D) 4.66920'], correct: 'A', explanation: 'π (pi) is approximately 3.14159, the ratio of circumference to diameter.' }, { question: 'What is the derivative of x²?', options: ['A) x', 'B) 2x', 'C) x³', 'D) 2x²'], correct: 'B', explanation: 'Using the power rule, the derivative of x² is 2x.' }, { question: 'What is the Pythagorean theorem?', options: ['A) a + b = c', 'B) a² + b² = c²', 'C) a × b = c', 'D) a² - b² = c²'], correct: 'B', explanation: 'The Pythagorean theorem states that a² + b² = c² for right triangles.' }, { question: 'What is the factorial of 5?', options: ['A) 25', 'B) 120', 'C) 60', 'D) 100'], correct: 'B', explanation: '5! = 5 × 4 × 3 × 2 × 1 = 120' }, { question: 'What is the square root of 64?', options: ['A) 6', 'B) 7', 'C) 8', 'D) 9'], correct: 'C', explanation: 'The square root of 64 is 8 because 8² = 64.' } ] }; // Get appropriate questions for the topic const availableQuestions = topicQuestions[topic.toLowerCase()] || topicQuestions['math']; for (let i = 0; i < questionCount; i++) { const questionIndex = i % availableQuestions.length; const baseQuestion = availableQuestions[questionIndex]; if (quizType === 'multiple-choice') { questions.push({ question: baseQuestion.question, options: baseQuestion.options, correct: baseQuestion.correct, explanation: baseQuestion.explanation }); } else if (quizType === 'true-false') { questions.push({ question: baseQuestion.question.replace(/Which|What|How/, 'Is it true that'), correct: Math.random() > 0.5 ? 'True' : 'False', explanation: baseQuestion.explanation }); } else { questions.push({ question: baseQuestion.question, answer: baseQuestion.correct, keywords: [baseQuestion.correct.toLowerCase(), topic.toLowerCase()] }); } } return questions; } app.post('/api/submit-quiz', requireAuth, async (req, res) => { try { const { answers, quiz, topic, difficulty, quizType } = req.body; let score = 0; const results = []; quiz.forEach((question, index) => { const userAnswer = answers[index]; const isCorrect = userAnswer === question.correct; if (isCorrect) score++; results.push({ question: question.question, userAnswer: userAnswer, correctAnswer: question.correct, isCorrect: isCorrect, explanation: question.explanation || '', options: question.options || null // Include options for multiple choice }); }); const percentage = Math.round((score / quiz.length) * 100); // Store quiz result in session if (!req.session.quizResults) { req.session.quizResults = []; } const quizResult = { id: uuidv4(), topic: topic, score: score, total: quiz.length, percentage: percentage, date: new Date().toISOString(), results: results, userId: req.session.userId, difficulty: difficulty || 'beginner', quizType: quizType || 'multiple-choice' }; // Store quiz result in session (for immediate use) req.session.quizResults.push(quizResult); // Store quiz result persistently await addQuizResult(req.session.userId, quizResult); console.log(`Quiz result saved for user ${req.session.userId}: ${score}/${quiz.length} (${percentage}%)`); res.json({ success: true, score: score, total: quiz.length, percentage: percentage, results: results, quizId: quizResult.id }); } catch (error) { console.error('Quiz submission error:', error); res.status(500).json({ error: 'Failed to submit quiz', details: error.message }); } }); // Quiz results persistence const QUIZ_RESULTS_FILE = path.join(__dirname, 'data', 'quiz-results.json'); // Ensure data directory exists async function ensureDataDirectory() { await fs.ensureDir(path.join(__dirname, 'data')); } // Load quiz results from file async function loadQuizResults() { try { await ensureDataDirectory(); if (await fs.pathExists(QUIZ_RESULTS_FILE)) { const data = await fs.readJSON(QUIZ_RESULTS_FILE); return data || {}; } return {}; } catch (error) { console.error('Error loading quiz results:', error); return {}; } } // Save quiz results to file async function saveQuizResults(results) { try { await ensureDataDirectory(); await fs.writeJSON(QUIZ_RESULTS_FILE, results, { spaces: 2 }); } catch (error) { console.error('Error saving quiz results:', error); } } // Add quiz result for user async function addQuizResult(userId, quizResult) { const allResults = await loadQuizResults(); if (!allResults[userId]) { allResults[userId] = []; } allResults[userId].push(quizResult); await saveQuizResults(allResults); } // Get quiz results for user async function getUserQuizResults(userId) { const allResults = await loadQuizResults(); return allResults[userId] || []; } // Error handling middleware app.use((error, req, res, next) => { if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'File too large. Maximum size is 10MB.' }); } } console.error('Error:', error); res.status(500).json({ error: 'Internal server error' }); }); // Quiz history route app.get('/quiz-history', requireAuth, async (req, res) => { try { const quizResults = await getUserQuizResults(req.session.userId); res.render('quiz-history', { title: 'Quiz History - EduCat', quizResults: quizResults.reverse() // Show newest first }); } catch (error) { console.error('Error loading quiz history:', error); res.render('quiz-history', { title: 'Quiz History - EduCat', quizResults: [] }); } }); // API route to clear quiz history app.delete('/api/quiz-history', requireAuth, async (req, res) => { try { const userId = req.session.userId; const allResults = await loadQuizResults(); // Clear quiz history for this user allResults[userId] = []; await saveQuizResults(allResults); console.log(`Quiz history cleared for user ${userId}`); res.json({ success: true, message: 'Quiz history cleared successfully' }); } catch (error) { console.error('Error clearing quiz history:', error); res.status(500).json({ success: false, error: 'Failed to clear quiz history' }); } }); // API route to get quiz statistics app.get('/api/quiz-stats', requireAuth, async (req, res) => { try { const quizResults = await getUserQuizResults(req.session.userId); if (quizResults.length === 0) { return res.json({ success: true, stats: { totalQuizzes: 0, averageScore: 0, bestScore: 0, recentQuizzes: [], topicStats: {}, progressChart: [] } }); } // Calculate statistics const totalQuizzes = quizResults.length; const averageScore = Math.round(quizResults.reduce((sum, quiz) => sum + quiz.percentage, 0) / totalQuizzes); const bestScore = Math.max(...quizResults.map(quiz => quiz.percentage)); const recentQuizzes = quizResults.slice(-5).reverse(); // Last 5 quizzes // Topic statistics const topicStats = {}; quizResults.forEach(quiz => { if (!topicStats[quiz.topic]) { topicStats[quiz.topic] = { count: 0, totalScore: 0, bestScore: 0 }; } topicStats[quiz.topic].count++; topicStats[quiz.topic].totalScore += quiz.percentage; topicStats[quiz.topic].bestScore = Math.max(topicStats[quiz.topic].bestScore, quiz.percentage); }); // Calculate average for each topic Object.keys(topicStats).forEach(topic => { topicStats[topic].averageScore = Math.round(topicStats[topic].totalScore / topicStats[topic].count); }); // Progress chart data (last 10 quizzes) const progressChart = quizResults.slice(-10).map((quiz, index) => ({ quiz: index + 1, score: quiz.percentage, topic: quiz.topic, date: quiz.date })); res.json({ success: true, stats: { totalQuizzes, averageScore, bestScore, recentQuizzes, topicStats, progressChart } }); } catch (error) { console.error('Error getting quiz stats:', error); res.status(500).json({ success: false, error: 'Failed to get quiz statistics' }); } }); // API route to get detailed quiz results by ID app.get('/api/quiz-details/:quizId', requireAuth, async (req, res) => { try { const quizId = req.params.quizId; const quizResults = await getUserQuizResults(req.session.userId); const quiz = quizResults.find(q => q.id === quizId); if (!quiz) { return res.status(404).json({ success: false, error: 'Quiz not found' }); } res.json({ success: true, quiz: quiz }); } catch (error) { console.error('Error getting quiz details:', error); res.status(500).json({ success: false, error: 'Failed to get quiz details' }); } }); // File progress tracking endpoint app.get('/api/files/:fileId/progress', requireAuth, async (req, res) => { try { const fileId = req.params.fileId; const files = req.session.uploadedFiles || []; const file = files.find(f => f.id === fileId); if (!file) { return res.status(404).json({ success: false, error: 'File not found' }); } res.json({ success: true, progress: { status: file.status, processingProgress: file.processingProgress || null, processingResult: file.processingResult || null, processingError: file.processingError || null } }); } catch (error) { console.error('Error getting file progress:', error); res.status(500).json({ success: false, error: 'Failed to get file progress', details: error.message }); } }); // 404 handler app.use((req, res) => { res.status(404).render('error', { title: 'Page Not Found', error: 'The page you are looking for does not exist.' }); }); app.listen(PORT, () => { console.log(`EduCat server running on http://localhost:${PORT}`); console.log(`Flowise API URL: ${FLOWISE_API_URL}`); console.log(`Flowise Chatflow ID: ${FLOWISE_CHATFLOW_ID}`); console.log(`Flowise Document Store ID: ${FLOWISE_DOCUMENT_STORE_ID}`); });