Files
EduCatWeb/server.js

1629 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) <!-- comment -->', '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}`);
});