Files
EduCatWeb/server.js
2025-07-12 14:45:30 +08:00

2924 lines
90 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');
const mammoth = require('mammoth'); // For .docx files
const pdfParse = require('pdf-parse'); // For PDF files
const ExcelJS = require('exceljs'); // For Excel files
// Markdown and HTML processing
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
// Initialize DOMPurify
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
// Dynamic import for marked (ES module)
let marked = null;
async function initializeMarked() {
if (!marked) {
const markedModule = await import('marked');
marked = markedModule.marked;
}
return marked;
}
// Helper function to extract text from various document formats
async function extractTextFromDocument(filePath, fileExtension) {
try {
const extension = fileExtension.toLowerCase();
switch (extension) {
case '.docx':
// Extract text from Word documents
const docxBuffer = await fs.readFile(filePath);
const docxResult = await mammoth.extractRawText({ buffer: docxBuffer });
return {
success: true,
text: docxResult.value,
extractedLength: docxResult.value.length,
method: 'mammoth'
};
case '.pdf':
// Extract text from PDF documents
const pdfBuffer = await fs.readFile(filePath);
const pdfResult = await pdfParse(pdfBuffer);
return {
success: true,
text: pdfResult.text,
extractedLength: pdfResult.text.length,
method: 'pdf-parse',
pages: pdfResult.numpages
};
case '.xlsx':
case '.xls':
// Extract text from Excel files using ExcelJS
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(filePath);
let excelText = '';
let sheetCount = 0;
workbook.eachSheet((worksheet, sheetId) => {
sheetCount++;
excelText += `Sheet: ${worksheet.name}\n`;
worksheet.eachRow((row, rowNumber) => {
const rowText = [];
row.eachCell((cell, colNumber) => {
// Extract cell value as text
let cellValue = cell.value;
if (cellValue !== null && cellValue !== undefined) {
// Handle different cell types
if (typeof cellValue === 'object' && cellValue.text) {
// Rich text object
cellValue = cellValue.text;
} else if (typeof cellValue === 'object' && cellValue.result) {
// Formula result
cellValue = cellValue.result;
}
rowText.push(String(cellValue));
}
});
if (rowText.length > 0) {
excelText += rowText.join('\t') + '\n';
}
});
excelText += '\n';
});
return {
success: true,
text: excelText,
extractedLength: excelText.length,
method: 'exceljs',
sheets: sheetCount
};
case '.txt':
case '.md':
case '.json':
case '.js':
case '.html':
case '.css':
case '.xml':
case '.csv':
// Read text files directly
const textContent = await fs.readFile(filePath, 'utf-8');
return {
success: true,
text: textContent,
extractedLength: textContent.length,
method: 'direct'
};
default:
return {
success: false,
error: `Unsupported file type: ${extension}`,
text: null
};
}
} catch (error) {
console.error('Error extracting text from document:', error);
return {
success: false,
error: error.message,
text: null
};
}
}
// 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-2025',
resave: true,
saveUninitialized: true,
rolling: true,
cookie: {
secure: false,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days instead of 1 day
httpOnly: true
},
name: 'educat.session.id' // Custom session name
}));
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 and load persistent files
app.use(async (req, res, next) => {
res.locals.user = req.session.userId ? users.find(u => u.id === req.session.userId) : null;
res.locals.messages = req.flash();
// Load user files from persistent storage if user is logged in and session doesn't have files
if (req.session.userId && (!req.session.uploadedFiles || req.session.uploadedFiles.length === 0)) {
try {
const persistentFiles = await loadUserFiles(req.session.userId);
if (persistentFiles.length > 0) {
req.session.uploadedFiles = persistentFiles;
}
} catch (error) {
console.error('Error loading user files in middleware:', error);
}
}
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) => {
// Define allowed file types for RAG processing - only text-extractable documents
const allowedExtensions = ['.pdf', '.txt', '.doc', '.docx', '.xlsx', '.xls', '.md', '.json', '.csv', '.xml'];
const allowedMimeTypes = [
'application/pdf',
'text/plain',
'text/markdown',
'text/csv',
'text/xml',
'application/xml',
'application/json',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
const fileExtension = path.extname(file.originalname).toLowerCase();
const fileMimeType = file.mimetype.toLowerCase();
// Check if file extension is allowed
const isExtensionAllowed = allowedExtensions.includes(fileExtension);
// Check if MIME type is allowed
const isMimeTypeAllowed = allowedMimeTypes.includes(fileMimeType);
// Additional validation for specific file types
if (isExtensionAllowed && isMimeTypeAllowed) {
return cb(null, true);
} else {
// Provide specific error messages for different rejection reasons
if (!isExtensionAllowed) {
return cb(new Error(`File type "${fileExtension}" is not supported. Only document files (PDF, Word, Excel, text files) are allowed for RAG processing.`));
} else if (!isMimeTypeAllowed) {
return cb(new Error(`MIME type "${fileMimeType}" is not supported. Please upload valid document files only.`));
} else {
return cb(new Error('Invalid file type. Only text-extractable documents are allowed to prevent RAG corruption.'));
}
}
}
});
// 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 {
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
});
// 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;
} else if (typeof response.data.loaders === 'string') {
// Loaders are a JSON string that needs parsing
try {
loaders = JSON.parse(response.data.loaders);
} catch (parseError) {
loaders = [];
}
} else {
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) {
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;
} else {
// Create a new loader by letting Flowise auto-generate or create new store
}
// 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}`;
// Prepare headers
const headers = {
...formData.getHeaders()
};
if (FLOWISE_API_KEY) {
headers['Authorization'] = `Bearer ${FLOWISE_API_KEY}`;
}
// 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
});
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
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 });
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('/');
});
});
// Dashboard route
app.get('/dashboard', requireAuth, async (req, res) => {
try {
// Load user files from both session and persistent storage
let allFiles = [];
let revisedFiles = [];
// Load persistent files
try {
const persistentFiles = await loadUserFiles(req.session.userId);
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
allFiles = persistentFiles;
revisedFiles = persistentRevisedFiles;
// Merge with session data (session data is more current)
const sessionFiles = req.session.uploadedFiles || [];
const sessionRevisedFiles = req.session.revisedFiles || [];
// Update session with persistent data if session is empty
if (sessionFiles.length === 0 && persistentFiles.length > 0) {
req.session.uploadedFiles = persistentFiles;
}
if (sessionRevisedFiles.length === 0 && persistentRevisedFiles.length > 0) {
req.session.revisedFiles = persistentRevisedFiles;
}
// Use session data as the source of truth for display
allFiles = req.session.uploadedFiles || persistentFiles;
revisedFiles = req.session.revisedFiles || persistentRevisedFiles;
} catch (error) {
console.error('Error loading user files for dashboard:', error);
// Fall back to session data
allFiles = req.session.uploadedFiles || [];
revisedFiles = req.session.revisedFiles || [];
}
res.render('dashboard', {
title: 'Dashboard - EduCat',
files: allFiles,
revisedFiles: revisedFiles
});
} catch (error) {
console.error('Dashboard error:', error);
req.flash('error', 'Error loading dashboard');
res.redirect('/');
}
});
// Chat route
app.get('/chat', requireAuth, async (req, res) => {
try {
// Initialize chat session ID if it doesn't exist
if (!req.session.chatSessionId) {
req.session.chatSessionId = `educat-${req.session.userId}-${Date.now()}`;
}
// Initialize chat history in session if it doesn't exist
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
res.render('chat', {
title: 'Chat with EduCat AI',
chatHistory: req.session.chatHistory
});
} catch (error) {
console.error('Chat route error:', error);
res.render('chat', {
title: 'Chat with EduCat AI',
chatHistory: []
});
}
});
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 {
if (!req.file) {
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'
};
// Store initial file info in session for immediate access
if (!req.session.uploadedFiles) {
req.session.uploadedFiles = [];
}
req.session.uploadedFiles.push(fileInfo);
// Also store in persistent storage
await addUserFile(req.session.userId, fileInfo);
// Send immediate response to prevent timeout
res.json({
success: true,
message: 'File uploaded successfully! Processing document...',
fileInfo: fileInfo,
processing: true
});
// 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 {
// Find the file in the session to update it by reference
const sessionFile = session.uploadedFiles.find(f => f.id === fileInfo.id);
if (!sessionFile) {
return;
}
// 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
};
// Initialize processing result
sessionFile.processingResult = {
totalChunks: 1, // FormData uploads are single operations
successfulChunks: 0,
failedChunks: 0,
startedAt: new Date().toISOString(),
status: 'uploading_to_flowise'
};
// Upsert document to Flowise using FormData
const upsertResult = await upsertDocumentToFlowiseFormData(fileInfo, documentMetadata);
// 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;
}
// Update persistent storage
await updateUserFile(userId, fileInfo.id, {
status: sessionFile.status,
processingResult: sessionFile.processingResult,
processingErrors: sessionFile.processingErrors
});
// Verify the session file was updated
// 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 {
}
});
// Log final upsert verification
} 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();
// Update persistent storage
await updateUserFile(userId, fileInfo.id, {
status: 'failed',
processingError: error.message,
processingResult: sessionFile.processingResult
});
// 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 {
}
});
}
}
}
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'
});
}
const filePath = path.join(__dirname, file.path);
const fileExtension = path.extname(file.originalName).toLowerCase();
// Extract text from the document using the same method as file preview
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
let fileContent = '';
let extractionInfo = null;
let extractionError = null;
if (extractionResult.success) {
fileContent = extractionResult.text;
extractionInfo = {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null
};
} else {
// Try to read as plain text if extraction failed
try {
fileContent = await fs.readFile(filePath, 'utf-8');
extractionInfo = { method: 'fallback-text', totalLength: fileContent.length };
} catch (readError) {
extractionError = `Failed to extract or read file: ${extractionResult.error}`;
fileContent = `Error: Unable to read file content. ${extractionResult.error}`;
}
}
res.render('revise', {
title: 'Revise Notes - EduCat',
file: file,
content: fileContent,
extractionInfo: extractionInfo,
extractionError: extractionError
});
} 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;
}
} 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.json({
success: false,
error: 'Failed to revise notes. Please try again.',
details: error.message
});
}
});
// Chat API endpoint
app.post('/api/chat', requireAuth, async (req, res) => {
try {
const { message, history = [] } = req.body;
if (!message || message.trim().length === 0) {
return res.json({
success: false,
error: 'Message is required'
});
}
// Initialize chat history in session if it doesn't exist
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
// Initialize or get persistent chat session ID for this user
if (!req.session.chatSessionId) {
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
}
// Prepare the request payload for Flowise with sessionId and chatId
const flowisePayload = {
question: message,
history: req.session.chatHistory,
sessionId: req.session.chatSessionId
};
// Add chatId if we have one from previous conversations
if (req.session.chatId) {
flowisePayload.chatId = req.session.chatId;
}
// Call Flowise API for chat with session history and sessionId
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, flowisePayload);
const aiResponse = response.data.text || response.data.answer || 'No response received';
// Save the chatId from Flowise response for future requests
if (response.data.chatId) {
req.session.chatId = response.data.chatId;
}
// Add the conversation to session history
req.session.chatHistory.push({
human: message,
ai: aiResponse,
timestamp: new Date().toISOString()
});
// Save session
req.session.save((err) => {
if (err) {
console.error('Error saving session:', err);
}
});
res.json({
success: true,
response: aiResponse
});
} catch (error) {
console.error('Chat error:', error);
res.json({
success: false,
error: 'Failed to get response from AI. Please try again.',
details: error.message
});
}
});
// Get chat history endpoint
app.get('/api/chat/history', requireAuth, (req, res) => {
try {
const chatHistory = req.session.chatHistory || [];
res.json({
success: true,
chatHistory: chatHistory
});
} catch (error) {
console.error('Error getting chat history:', error);
res.json({
success: false,
error: 'Failed to get chat history',
details: error.message
});
}
});
// Delete chat history endpoint
app.delete('/api/chat/history', requireAuth, (req, res) => {
try {
req.session.chatHistory = [];
// Reset the session ID to start a fresh conversation
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
// Clear the Flowise chatId
delete req.session.chatId;
req.session.save((err) => {
if (err) {
console.error('Error clearing chat session:', err);
return res.json({
success: false,
error: 'Failed to clear chat history'
});
}
res.json({
success: true,
message: 'Chat history cleared'
});
});
} catch (error) {
console.error('Error clearing chat history:', error);
res.json({
success: false,
error: 'Failed to clear chat history',
details: error.message
});
}
});
// Save revised notes endpoint
app.post('/api/save-revised', requireAuth, async (req, res) => {
try {
const { fileId, revisedContent, revisionType } = req.body;
if (!fileId || !revisedContent) {
return res.json({
success: false,
error: 'File ID and revised content are required'
});
}
// Find the original file
const files = req.session.uploadedFiles || [];
const originalFile = files.find(f => f.id === fileId);
if (!originalFile) {
return res.json({
success: false,
error: 'Original file not found'
});
}
// Create revised notes directory if it doesn't exist
const revisedNotesDir = path.join(__dirname, 'uploads', 'revised-notes');
await fs.ensureDir(revisedNotesDir);
// Create revised file info
const timestamp = Date.now();
const originalExt = path.extname(originalFile.originalName);
const fileName = `${path.parse(originalFile.originalName).name}_${revisionType}_revised_${timestamp}.txt`;
const filePath = path.join(revisedNotesDir, fileName);
const revisedFileInfo = {
id: uuidv4(),
originalName: fileName,
filename: fileName,
path: filePath,
size: Buffer.byteLength(revisedContent, 'utf8'),
mimetype: 'text/plain',
uploadDate: new Date().toISOString(),
userId: req.session.userId,
status: 'processed',
isRevised: true,
originalFileId: fileId,
revisionType: revisionType,
originalFileName: originalFile.originalName
};
// Save revised content to file
await fs.writeFile(revisedFileInfo.path, revisedContent, 'utf8');
// Don't add revised files to regular uploadedFiles - store separately
if (!req.session.revisedFiles) {
req.session.revisedFiles = [];
}
req.session.revisedFiles.push(revisedFileInfo);
// Save to persistent storage in a separate category
await saveRevisedFile(req.session.userId, revisedFileInfo);
res.json({
success: true,
message: 'Revised notes saved successfully!',
fileInfo: revisedFileInfo
});
} catch (error) {
console.error('Save revised notes error:', error);
res.json({
success: false,
error: 'Failed to save revised notes',
details: error.message
});
}
});
// Download revised notes endpoint
app.get('/api/download-revised/:fileId', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const { content } = req.query;
if (!content) {
return res.status(400).json({
success: false,
error: 'No content provided for download'
});
}
// Find the original file to get naming info
const files = req.session.uploadedFiles || [];
const originalFile = files.find(f => f.id === fileId);
// Get the revision type from the query if available
const revisionType = req.query.revisionType || 'revised';
const fileName = originalFile
? `${path.parse(originalFile.originalName).name}_${revisionType}.txt`
: `revised_notes_${Date.now()}.txt`;
// Set headers for file download
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
res.setHeader('Content-Length', Buffer.byteLength(content, 'utf8'));
// Send the content
res.send(content);
} catch (error) {
console.error('Download revised notes error:', error);
res.status(500).json({
success: false,
error: 'Failed to download revised notes',
details: error.message
});
}
});
// Serve revised files for direct download
app.get('/uploads/revised-notes/:filename', requireAuth, (req, res) => {
try {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'uploads', 'revised-notes', filename);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
error: 'File not found'
});
}
// Serve the file
res.download(filePath, filename);
} catch (error) {
console.error('Error serving revised file:', error);
res.status(500).json({
success: false,
error: 'Failed to serve file'
});
}
});
// Delete revised file endpoint
app.delete('/api/revised-files/:fileId', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const revisedFiles = req.session.revisedFiles || [];
const fileIndex = revisedFiles.findIndex(f => f.id === fileId);
if (fileIndex === -1) {
return res.status(404).json({ success: false, error: 'File not found' });
}
const file = revisedFiles[fileIndex];
// Delete the physical file
if (await fs.pathExists(file.path)) {
await fs.unlink(file.path);
}
// Remove from session
req.session.revisedFiles.splice(fileIndex, 1);
// Remove from persistent storage
await removeRevisedFile(req.session.userId, fileId);
res.json({
success: true,
message: 'Revised file deleted successfully'
});
} catch (error) {
console.error('Error deleting revised file:', error);
res.status(500).json({
success: false,
error: 'Failed to delete file',
details: error.message
});
}
});
// Get revised file content with rendering options
app.get('/api/revised-files/:fileId/content', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const { displayMode = 'markdown' } = req.query;
const revisedFiles = req.session.revisedFiles || [];
const file = revisedFiles.find(f => f.id === fileId);
if (!file) {
// Try to load from persistent storage
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
const persistentFile = persistentRevisedFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({
success: false,
error: 'File not found'
});
}
// Read file content
const content = await fs.readFile(persistentFile.path, 'utf8');
return res.json({
success: true,
file: persistentFile,
content: content,
displayMode: displayMode
});
}
// Read file content
const content = await fs.readFile(file.path, 'utf8');
res.json({
success: true,
file: file,
content: content,
displayMode: displayMode
});
} catch (error) {
console.error('Error getting revised file content:', error);
res.status(500).json({
success: false,
error: 'Failed to get file content',
details: error.message
});
}
});
// Get revised file info endpoint
app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const revisedFiles = req.session.revisedFiles || [];
const file = revisedFiles.find(f => f.id === fileId);
if (!file) {
// Try to load from persistent storage
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
const persistentFile = persistentRevisedFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({
success: false,
error: 'File not found'
});
}
return res.json({
success: true,
file: persistentFile
});
}
res.json({
success: true,
file: file
});
} catch (error) {
console.error('Error getting revised file info:', error);
res.status(500).json({
success: false,
error: 'Failed to get file info',
details: error.message
});
}
});
// Render revised notes content endpoint
app.post('/api/render-revised-content', requireAuth, async (req, res) => {
try {
const { content, displayMode = 'markdown', autoDetect = true } = req.body;
if (!content) {
return res.json({
success: false,
error: 'No content provided'
});
}
let renderedContent = '';
let detectedFormat = 'text';
let isMarkdownContent = false;
// Auto-detect if content is markdown (if autoDetect is enabled)
if (autoDetect) {
isMarkdownContent = isLikelyMarkdown(content);
detectedFormat = isMarkdownContent ? 'markdown' : 'text';
}
// Process content based on display mode
switch (displayMode) {
case 'html':
if (isMarkdownContent || autoDetect === false) {
// Convert markdown to safe HTML
renderedContent = await markdownToSafeHtml(content);
} else {
// Just escape HTML and preserve line breaks for plain text
renderedContent = escapeHtml(content).replace(/\n/g, '<br>');
}
break;
case 'markdown':
case 'raw':
default:
// Return raw content (for markdown view or plain text)
renderedContent = content;
break;
}
res.json({
success: true,
renderedContent: renderedContent,
displayMode: displayMode,
detectedFormat: detectedFormat,
isMarkdownContent: isMarkdownContent
});
} catch (error) {
console.error('Error rendering content:', error);
res.status(500).json({
success: false,
error: 'Failed to render content',
details: error.message
});
}
});
// Get file preview endpoint (simplified for dashboard)
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) {
// Try to load from persistent storage
try {
const persistentFiles = await loadUserFiles(req.session.userId);
const persistentFile = persistentFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({ success: false, error: 'File not found' });
}
// Process the persistent file
const filePath = path.join(__dirname, persistentFile.path);
const fileExtension = path.extname(persistentFile.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${persistentFile.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
const extractedText = extractionResult.text;
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
} catch (error) {
console.error('Error loading from persistent storage:', error);
return res.status(404).json({ success: false, error: 'File not found' });
}
}
const filePath = path.join(__dirname, file.path);
const fileExtension = path.extname(file.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${file.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
// Successfully extracted text
const extractedText = extractionResult.text;
// Limit preview to first 5000 characters to avoid huge responses
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
// Failed to extract text, fall back to file type detection
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
if (textFormats.includes(fileExtension)) {
// Try reading as plain text (should have been handled by extraction, but fallback)
try {
const fileContent = await fs.readFile(filePath, 'utf-8');
const previewContent = fileContent.length > 5000 ?
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
fileContent;
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: previewContent,
previewType: 'text'
}
});
} catch (readError) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File preview not available',
previewType: 'error',
message: `Error reading ${fileExtension.toUpperCase()} file: ${readError.message}`
}
});
}
} else {
// Binary format that couldn't be processed
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
}
} catch (error) {
console.error('Error getting file preview:', error);
res.status(500).json({
success: false,
error: 'Failed to get file preview',
details: error.message
});
}
});
// Get file preview content endpoint (for notes and revisions)
app.get('/api/files/:fileId/preview-content', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const { displayMode = 'markdown' } = req.query;
const files = req.session.uploadedFiles || [];
const file = files.find(f => f.id === fileId);
if (!file) {
// Try to load from persistent storage
try {
const persistentFiles = await loadUserFiles(req.session.userId);
const persistentFile = persistentFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({ success: false, error: 'File not found' });
}
// Process the persistent file
const filePath = path.join(__dirname, persistentFile.path);
const fileExtension = path.extname(persistentFile.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${persistentFile.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
const extractedText = extractionResult.text;
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
} catch (error) {
console.error('Error loading from persistent storage:', error);
return res.status(404).json({ success: false, error: 'File not found' });
}
}
const filePath = path.join(__dirname, file.path);
const fileExtension = path.extname(file.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${file.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
// Successfully extracted text
const extractedText = extractionResult.text;
// Limit preview to first 5000 characters to avoid huge responses
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
// Failed to extract text, fall back to file type detection
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
if (textFormats.includes(fileExtension)) {
// Try reading as plain text (should have been handled by extraction, but fallback)
try {
const fileContent = await fs.readFile(filePath, 'utf-8');
const previewContent = fileContent.length > 5000 ?
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
fileContent;
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: previewContent,
previewType: 'text'
}
});
} catch (readError) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File preview not available',
previewType: 'error',
message: `Error reading ${fileExtension.toUpperCase()} file: ${readError.message}`
}
});
}
} else {
// Binary format that couldn't be processed
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
}
} 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);
// Remove from persistent storage
await removeUserFile(req.session.userId, 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;
let files = req.session.uploadedFiles || [];
let file = files.find(f => f.id === fileId);
// If not in session, try to load from persistent storage
if (!file) {
const persistentFiles = await loadUserFiles(req.session.userId);
file = persistentFiles.find(f => f.id === fileId);
// If found in persistent storage, add to session for future requests
if (file) {
if (!req.session.uploadedFiles) {
req.session.uploadedFiles = [];
}
req.session.uploadedFiles.push(file);
}
}
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 {
// Load persistent files and merge with session
const persistentFiles = await loadUserFiles(req.session.userId);
const sessionFiles = req.session.uploadedFiles || [];
// Merge files, preferring session data for current uploads
const allFiles = [...persistentFiles];
sessionFiles.forEach(sessionFile => {
const existingIndex = allFiles.findIndex(f => f.id === sessionFile.id);
if (existingIndex !== -1) {
// Update existing with session data (more current)
allFiles[existingIndex] = sessionFile;
} else {
// Add new session file
allFiles.push(sessionFile);
}
});
const statusSummary = allFiles.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: allFiles.length,
processing: allFiles.filter(f => f.status === 'processing').length,
processed: allFiles.filter(f => f.status === 'processed').length,
failed: allFiles.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) => {
try {
const { topic, difficulty, questionCount, quizType } = req.body;
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",
"explanation": "Photosynthesis is the process by which plants use sunlight, water, and carbon dioxide to produce glucose and oxygen.",
"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.`;
}
// Call Flowise API with retry logic for better AI responses
let quizData;
let retryCount = 0;
const maxRetries = 2;
while (retryCount <= maxRetries) {
try {
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
question: retryCount > 0 ?
`${prompt}\n\nIMPORTANT: Please provide ONLY a valid JSON array of questions. Do not include any explanatory text, greetings, or additional commentary. Start your response directly with [ and end with ].` :
prompt,
history: []
});
const responseText = response.data.text || response.data.answer || response.data;
// Check if the AI response looks like it's not following instructions
if (responseText.toLowerCase().includes('do you have a question') ||
responseText.toLowerCase().includes('would you like me to help') ||
responseText.toLowerCase().includes('something else related') ||
responseText.toLowerCase().includes('how can i help') ||
(!responseText.includes('[') && !responseText.includes('{'))) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`AI gave improper response, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error('The AI is not responding properly to quiz generation requests. Please try again with a more specific topic or try again later.');
}
}
// 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];
} 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);
}
}
}
if (!jsonString) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Could not find JSON in response, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error('Could not find valid JSON in the AI response after multiple attempts. Please try generating the quiz again.');
}
}
quizData = JSON.parse(jsonString);
// Validate the parsed data
if (!Array.isArray(quizData) || quizData.length === 0) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Invalid quiz format received, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error('The AI generated an invalid quiz format after multiple attempts. Please try again.');
}
}
// Validate each question has required fields
for (let i = 0; i < quizData.length; i++) {
const q = quizData[i];
if (!q.question || (!q.correct && !q.answer)) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Question ${i + 1} missing required fields, retrying... (attempt ${retryCount + 1})`);
continue;
} else {
throw new Error(`Question ${i + 1} is missing required fields after multiple attempts. Please try generating the quiz again.`);
}
}
}
// If we get here, the quiz is valid
break;
} catch (parseError) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`Parse error occurred, retrying... (attempt ${retryCount + 1}):`, parseError.message);
continue;
} else {
console.error('Quiz parsing error after retries:', parseError);
console.error('AI Response was:', response?.data?.text || response?.data?.answer || response?.data || 'No response');
// Return error instead of fallback
return res.json({
success: false,
error: parseError.message || 'Failed to parse quiz questions from AI response after multiple attempts. Please try again with a different topic or rephrase your request.'
});
}
}
}
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 error instead of fallback quiz
res.json({
success: false,
error: 'Failed to generate quiz. Please check your connection and try again, or try a different topic.'
});
}
});
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];
// Handle different question types - short-answer uses 'answer', others use 'correct'
const correctAnswer = question.answer || question.correct;
const isCorrect = userAnswer === correctAnswer;
if (isCorrect) score++;
results.push({
question: question.question,
userAnswer: userAnswer,
correctAnswer: correctAnswer,
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);
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
});
}
});
// User file storage persistence
const USER_FILES_DIR = path.join(__dirname, 'data', 'user-files');
// Ensure user files directory exists
async function ensureUserFilesDirectory() {
await fs.ensureDir(USER_FILES_DIR);
}
// Save user files to persistent storage
async function saveUserFiles(userId, files) {
try {
await ensureUserFilesDirectory();
const userFilePath = path.join(USER_FILES_DIR, `user-${userId}-files.json`);
await fs.writeJSON(userFilePath, files, { spaces: 2 });
} catch (error) {
console.error('Error saving user files:', error);
}
}
// Load user files from persistent storage
async function loadUserFiles(userId) {
try {
await ensureUserFilesDirectory();
const userFilePath = path.join(USER_FILES_DIR, `user-${userId}-files.json`);
if (await fs.pathExists(userFilePath)) {
const files = await fs.readJSON(userFilePath);
return files || [];
}
return [];
} catch (error) {
console.error('Error loading user files:', error);
return [];
}
}
// Add a file to user's persistent storage
async function addUserFile(userId, fileInfo) {
const userFiles = await loadUserFiles(userId);
userFiles.push(fileInfo);
await saveUserFiles(userId, userFiles);
}
// Update a file in user's persistent storage
async function updateUserFile(userId, fileId, updates) {
const userFiles = await loadUserFiles(userId);
const fileIndex = userFiles.findIndex(f => f.id === fileId);
if (fileIndex !== -1) {
userFiles[fileIndex] = { ...userFiles[fileIndex], ...updates };
await saveUserFiles(userId, userFiles);
}
}
// Remove a file from user's persistent storage
async function removeUserFile(userId, fileId) {
const userFiles = await loadUserFiles(userId);
const filteredFiles = userFiles.filter(f => f.id !== fileId);
await saveUserFiles(userId, filteredFiles);
}
// Revised files storage persistence
const REVISED_FILES_DIR = path.join(__dirname, 'data', 'revised-files');
// Ensure revised files directory exists
async function ensureRevisedFilesDirectory() {
await fs.ensureDir(REVISED_FILES_DIR);
}
// Save revised files to persistent storage
async function saveRevisedFiles(userId, files) {
try {
await ensureRevisedFilesDirectory();
const revisedFilePath = path.join(REVISED_FILES_DIR, `user-${userId}-revised.json`);
await fs.writeJSON(revisedFilePath, files, { spaces: 2 });
} catch (error) {
console.error('Error saving revised files:', error);
}
}
// Load revised files from persistent storage
async function loadRevisedFiles(userId) {
try {
await ensureRevisedFilesDirectory();
const revisedFilePath = path.join(REVISED_FILES_DIR, `user-${userId}-revised.json`);
if (await fs.pathExists(revisedFilePath)) {
const files = await fs.readJSON(revisedFilePath);
return files || [];
}
return [];
} catch (error) {
console.error('Error loading revised files:', error);
return [];
}
}
// Add a revised file to user's persistent storage
async function saveRevisedFile(userId, fileInfo) {
const revisedFiles = await loadRevisedFiles(userId);
revisedFiles.push(fileInfo);
await saveRevisedFiles(userId, revisedFiles);
}
// Remove a revised file from user's persistent storage
async function removeRevisedFile(userId, fileId) {
const revisedFiles = await loadRevisedFiles(userId);
const filteredFiles = revisedFiles.filter(f => f.id !== fileId);
await saveRevisedFiles(userId, filteredFiles);
}
// Quiz results storage persistence
const QUIZ_RESULTS_DIR = path.join(__dirname, 'data', 'quiz-results');
// Ensure quiz results directory exists
async function ensureQuizResultsDirectory() {
await fs.ensureDir(QUIZ_RESULTS_DIR);
}
// Save quiz results to persistent storage
async function saveQuizResults(allResults) {
try {
await ensureQuizResultsDirectory();
const resultsPath = path.join(QUIZ_RESULTS_DIR, 'quiz-results.json');
await fs.writeJSON(resultsPath, allResults, { spaces: 2 });
} catch (error) {
console.error('Error saving quiz results:', error);
throw error;
}
}
// Load all quiz results from persistent storage
async function loadQuizResults() {
try {
await ensureQuizResultsDirectory();
const resultsPath = path.join(QUIZ_RESULTS_DIR, 'quiz-results.json');
if (await fs.pathExists(resultsPath)) {
return await fs.readJSON(resultsPath);
} else {
return {};
}
} catch (error) {
console.error('Error loading quiz results:', error);
return {};
}
}
// Get quiz results for a specific user
async function getUserQuizResults(userId) {
try {
const allResults = await loadQuizResults();
return allResults[userId] || [];
} catch (error) {
console.error('Error loading user quiz results:', error);
return [];
}
}
// Add a quiz result to user's persistent storage
async function addQuizResult(userId, quizResult) {
try {
const allResults = await loadQuizResults();
if (!allResults[userId]) {
allResults[userId] = [];
}
allResults[userId].push(quizResult);
await saveQuizResults(allResults);
} catch (error) {
console.error('Error adding quiz result:', error);
throw error;
}
}
// Clear quiz results for a specific user
async function clearUserQuizResults(userId) {
try {
const allResults = await loadQuizResults();
allResults[userId] = [];
await saveQuizResults(allResults);
} catch (error) {
console.error('Error clearing user quiz results:', error);
throw error;
}
}
// 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;
// Clear quiz history for this user using the dedicated function
await clearUserQuizResults(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}`);
});
// Helper function to convert markdown to safe HTML
async function markdownToSafeHtml(markdownText) {
try {
// Initialize marked with dynamic import
const markedInstance = await initializeMarked();
// Configure marked options for better security and features
markedInstance.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert line breaks to <br>
sanitize: false, // We'll use DOMPurify instead for better control
smartLists: true,
smartypants: true,
highlight: null // No code highlighting to avoid security issues
});
// Convert markdown to HTML
const rawHtml = markedInstance.parse(markdownText);
// Sanitize the HTML with DOMPurify
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'b', 'em', 'i', 'u', 'mark',
'ul', 'ol', 'li',
'blockquote', 'pre', 'code',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'a', 'img',
'div', 'span'
],
ALLOWED_ATTR: [
'href', 'target', 'rel',
'src', 'alt', 'title',
'class', 'id',
'width', 'height'
],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'object', 'embed', 'iframe', 'form', 'input'],
FORBID_ATTR: ['onclick', 'onload', 'onerror', 'style'],
ADD_ATTR: {
'a': { 'target': '_blank', 'rel': 'noopener noreferrer' }
}
});
return cleanHtml;
} catch (error) {
console.error('Error converting markdown to HTML:', error);
// Return escaped plain text as fallback
return escapeHtml(markdownText);
}
}
// Helper function to escape HTML
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Helper function to detect if content is likely markdown
function isLikelyMarkdown(text) {
const markdownIndicators = [
/^#{1,6}\s+.+$/m, // Headers
/\*{1,2}[^*]+\*{1,2}/, // Bold/italic
/^[\s]*[-*+]\s+/m, // Bullet lists
/^\d+\.\s+/m, // Numbered lists
/```[\s\S]*?```/, // Code blocks
/`[^`]+`/, // Inline code
/\[.+\]\(.+\)/, // Links
/^\>.+$/m // Blockquotes
];
return markdownIndicators.some(pattern => pattern.test(text));
}
// Chat history storage persistence
const CHAT_HISTORY_DIR = path.join(__dirname, 'data', 'chat-history');
// Ensure chat history directory exists
async function ensureChatHistoryDirectory() {
await fs.ensureDir(CHAT_HISTORY_DIR);
}
// Save chat history to persistent storage
async function saveChatHistory(userId, chatHistory) {
try {
console.log(`Saving chat history for user ${userId}, ${chatHistory.length} messages`);
await ensureChatHistoryDirectory();
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
await fs.writeJSON(historyPath, chatHistory, { spaces: 2 });
console.log(`Chat history saved successfully to ${historyPath}`);
} catch (error) {
console.error('Error saving chat history:', error);
throw error;
}
}
// Load chat history from persistent storage
async function loadChatHistory(userId) {
try {
console.log(`Loading chat history for user ${userId}`);
await ensureChatHistoryDirectory();
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
if (await fs.pathExists(historyPath)) {
const history = await fs.readJSON(historyPath);
console.log(`Loaded ${history.length} chat messages for user ${userId}`);
return history;
} else {
console.log(`No chat history file found for user ${userId}`);
return [];
}
} catch (error) {
console.error('Error loading chat history:', error);
return [];
}
}
// Clear chat history for a user
async function clearChatHistory(userId) {
try {
await ensureChatHistoryDirectory();
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
if (await fs.pathExists(historyPath)) {
await fs.unlink(historyPath);
}
} catch (error) {
console.error('Error clearing chat history:', error);
throw error;
}
}
// Initialize directory structures on startup
async function initializeDataDirectories() {
await ensureUserFilesDirectory();
await ensureRevisedFilesDirectory();
await ensureQuizResultsDirectory();
await ensureChatHistoryDirectory();
}
// Call initialization
initializeDataDirectories().catch(console.error);
initializeDataDirectories().catch(console.error);