1761 lines
55 KiB
JavaScript
1761 lines
55 KiB
JavaScript
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, req.session);
|
||
|
||
} 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, session) {
|
||
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}`);
|
||
console.log(`Session ID in processing: ${session.id || 'undefined'}`);
|
||
|
||
// Find the file in the session to update it by reference
|
||
const sessionFile = session.uploadedFiles.find(f => f.id === fileInfo.id);
|
||
if (!sessionFile) {
|
||
console.error('File not found in session:', fileInfo.id);
|
||
console.error('Available files in session:', session.uploadedFiles.map(f => ({ id: f.id, name: f.originalName })));
|
||
return;
|
||
}
|
||
|
||
console.log('Found session file for update:', { id: sessionFile.id, name: sessionFile.originalName, currentStatus: sessionFile.status });
|
||
|
||
// 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
|
||
sessionFile.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 session file with processing results
|
||
sessionFile.status = upsertResult.success ? 'processed' : 'failed';
|
||
sessionFile.processingProgress = null; // Clear progress when done
|
||
sessionFile.processingResult = {
|
||
totalChunks: upsertResult.totalChunks,
|
||
successfulChunks: upsertResult.successfulChunks,
|
||
failedChunks: upsertResult.failedChunks,
|
||
processedAt: new Date().toISOString(),
|
||
startedAt: sessionFile.processingResult.startedAt,
|
||
duration: Date.now() - new Date(sessionFile.processingResult.startedAt).getTime(),
|
||
method: 'formdata'
|
||
};
|
||
|
||
if (upsertResult.errors.length > 0) {
|
||
sessionFile.processingErrors = upsertResult.errors;
|
||
console.log(`Processing errors for ${sessionFile.originalName}:`, upsertResult.errors);
|
||
}
|
||
|
||
console.log(`Document processing completed for: ${sessionFile.originalName}`);
|
||
console.log(`Result: FormData upload ${upsertResult.success ? 'successful' : 'failed'}`);
|
||
console.log(`Updated session file status to: ${sessionFile.status}`);
|
||
|
||
// Verify the session file was updated
|
||
console.log('Session file after update:', { id: sessionFile.id, name: sessionFile.originalName, status: sessionFile.status });
|
||
console.log('All session files after update:', session.uploadedFiles.map(f => ({ id: f.id, name: f.originalName, status: f.status })));
|
||
|
||
// Force session save since we modified it in an async context
|
||
session.save((err) => {
|
||
if (err) {
|
||
console.error('Error saving session after document processing:', err);
|
||
} else {
|
||
console.log('Session saved successfully after document processing');
|
||
}
|
||
});
|
||
|
||
// Log final upsert verification
|
||
console.log('Final FormData upsert verification:', {
|
||
documentStore: `${FLOWISE_BASE_URL}/api/v1/document-store/${FLOWISE_DOCUMENT_STORE_ID}`,
|
||
documentId: sessionFile.id,
|
||
fileName: sessionFile.originalName,
|
||
success: upsertResult.success,
|
||
finalStatus: sessionFile.status
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error(`Error processing document ${fileInfo.originalName}:`, error);
|
||
|
||
// Find the file in the session to update it with error
|
||
const sessionFile = session.uploadedFiles.find(f => f.id === fileInfo.id);
|
||
if (sessionFile) {
|
||
sessionFile.status = 'failed';
|
||
sessionFile.processingError = error.message;
|
||
sessionFile.processingResult = sessionFile.processingResult || {};
|
||
sessionFile.processingResult.failedAt = new Date().toISOString();
|
||
console.log(`Updated session file status to: ${sessionFile.status} due to error`);
|
||
|
||
// Force session save since we modified it in an async context
|
||
session.save((err) => {
|
||
if (err) {
|
||
console.error('Error saving session after processing error:', err);
|
||
} else {
|
||
console.log('Session saved successfully after processing error');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
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) => {
|
||
// Initialize chat history if it doesn't exist
|
||
if (!req.session.chatHistory) {
|
||
req.session.chatHistory = [];
|
||
}
|
||
|
||
res.render('chat', {
|
||
title: 'Chat with EduCat AI',
|
||
chatHistory: req.session.chatHistory
|
||
});
|
||
});
|
||
|
||
app.post('/api/chat', requireAuth, async (req, res) => {
|
||
try {
|
||
const { message } = req.body;
|
||
|
||
// Initialize chat history in session if it doesn't exist
|
||
if (!req.session.chatHistory) {
|
||
req.session.chatHistory = [];
|
||
}
|
||
|
||
console.log('Chat message received:', message);
|
||
console.log('Current chat history length:', req.session.chatHistory.length);
|
||
|
||
// Call Flowise API for chat with session history
|
||
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
|
||
question: message,
|
||
history: req.session.chatHistory
|
||
});
|
||
|
||
const aiResponse = response.data.text || response.data.answer || 'No response received';
|
||
|
||
// Add the conversation to session history
|
||
req.session.chatHistory.push({
|
||
human: message,
|
||
ai: aiResponse
|
||
});
|
||
|
||
// Save session explicitly since we modified it
|
||
req.session.save((err) => {
|
||
if (err) {
|
||
console.error('Error saving chat session:', err);
|
||
}
|
||
});
|
||
|
||
console.log('Updated chat history length:', req.session.chatHistory.length);
|
||
|
||
res.json({
|
||
success: true,
|
||
response: aiResponse
|
||
});
|
||
} catch (error) {
|
||
console.error('Chat error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to get chat response',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Get chat history from session
|
||
app.get('/api/chat/history', requireAuth, (req, res) => {
|
||
try {
|
||
const chatHistory = req.session.chatHistory || [];
|
||
console.log('Chat history requested, returning', chatHistory.length, 'messages');
|
||
|
||
res.json({
|
||
success: true,
|
||
history: chatHistory
|
||
});
|
||
} catch (error) {
|
||
console.error('Error getting chat history:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to get chat history',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Clear chat history
|
||
app.delete('/api/chat/history', requireAuth, (req, res) => {
|
||
try {
|
||
req.session.chatHistory = [];
|
||
req.session.save((err) => {
|
||
if (err) {
|
||
console.error('Error clearing chat session:', err);
|
||
return res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to clear chat history'
|
||
});
|
||
}
|
||
|
||
console.log('Chat history cleared for user:', req.session.userId);
|
||
res.json({
|
||
success: true,
|
||
message: 'Chat history cleared'
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error('Error clearing chat history:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to clear chat history',
|
||
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);
|
||
|
||
console.log(`Status requested for file ${fileId}`);
|
||
console.log(`Session ID: ${req.session.id || req.sessionID}`);
|
||
console.log(`User ID: ${req.session.userId}`);
|
||
console.log(`Files in session:`, files.map(f => ({ id: f.id, name: f.originalName, status: f.status })));
|
||
console.log(`Found file:`, file ? { id: file.id, name: file.originalName, status: file.status } : 'Not found');
|
||
|
||
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, req.session);
|
||
|
||
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 || [];
|
||
console.log('Status check requested for session files:', files.map(f => ({ id: f.id, name: f.originalName, status: f.status })));
|
||
|
||
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
|
||
};
|
||
|
||
console.log('Returning status summary:', summary);
|
||
|
||
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}`);
|
||
});
|
||
|
||
|
||
|
||
|