Add HTML & Markdown preview in dashboard and revise pages
This commit is contained in:
871
server.js
871
server.js
@@ -17,6 +17,15 @@ 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 { marked } = require('marked');
|
||||
const createDOMPurify = require('dompurify');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
// Initialize DOMPurify
|
||||
const window = new JSDOM('').window;
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
// Helper function to extract text from various document formats
|
||||
async function extractTextFromDocument(filePath, fileExtension) {
|
||||
try {
|
||||
@@ -705,6 +714,82 @@ app.get('/logout', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 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 {
|
||||
// Load user chat history
|
||||
let chatHistory = [];
|
||||
try {
|
||||
chatHistory = await loadChatHistory(req.session.userId);
|
||||
} catch (error) {
|
||||
console.error('Error loading chat history:', error);
|
||||
chatHistory = [];
|
||||
}
|
||||
|
||||
res.render('chat', {
|
||||
title: 'AI Chat - EduCat',
|
||||
chatHistory: chatHistory
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Chat route error:', error);
|
||||
res.render('chat', {
|
||||
title: 'AI Chat - EduCat',
|
||||
chatHistory: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/upload', requireAuth, (req, res) => {
|
||||
res.render('upload', {
|
||||
title: 'Upload Your Notes - EduCat'
|
||||
@@ -995,6 +1080,82 @@ app.post('/api/revise', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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'
|
||||
});
|
||||
}
|
||||
|
||||
// Load existing chat history from storage
|
||||
let existingHistory = [];
|
||||
try {
|
||||
existingHistory = await loadChatHistory(req.session.userId);
|
||||
} catch (error) {
|
||||
console.log('No existing chat history found, starting fresh');
|
||||
}
|
||||
|
||||
// Prepare history for API call (last 10 conversations)
|
||||
const recentHistory = existingHistory.slice(-10).map(conv => [
|
||||
{ role: 'human', content: conv.human },
|
||||
{ role: 'ai', content: conv.ai }
|
||||
]).flat();
|
||||
|
||||
// Call Flowise API for chat
|
||||
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
|
||||
question: message.trim(),
|
||||
history: recentHistory
|
||||
});
|
||||
|
||||
const botResponse = response.data.text || response.data.answer || 'Sorry, I could not process your request.';
|
||||
|
||||
// Save the conversation to history
|
||||
const conversation = {
|
||||
human: message.trim(),
|
||||
ai: botResponse,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
existingHistory.push(conversation);
|
||||
await saveChatHistory(req.session.userId, existingHistory);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
response: botResponse
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Chat error:', error);
|
||||
res.json({
|
||||
success: false,
|
||||
error: 'Failed to get response from AI. Please try again.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete chat history endpoint
|
||||
app.delete('/api/chat/history', requireAuth, async (req, res) => {
|
||||
try {
|
||||
await clearChatHistory(req.session.userId);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Chat history cleared successfully'
|
||||
});
|
||||
} 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 {
|
||||
@@ -1177,6 +1338,56 @@ app.delete('/api/revised-files/:fileId', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
@@ -1216,200 +1427,67 @@ app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ChatGPT integration routes
|
||||
app.get('/chat', requireAuth, (req, res) => {
|
||||
// Initialize chat history if it doesn't exist
|
||||
if (!req.session.chatHistory) {
|
||||
req.session.chatHistory = [];
|
||||
}
|
||||
|
||||
// Initialize chat session ID if it doesn't exist
|
||||
if (!req.session.chatSessionId) {
|
||||
req.session.chatSessionId = `educat-${req.session.userId}-${Date.now()}`;
|
||||
|
||||
}
|
||||
|
||||
res.render('chat', {
|
||||
title: 'Chat with EduCat AI',
|
||||
chatHistory: req.session.chatHistory
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
// Render revised notes content endpoint
|
||||
app.post('/api/render-revised-content', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
const { content, displayMode = 'markdown', autoDetect = true } = req.body;
|
||||
|
||||
// Initialize chat history in session if it doesn't exist
|
||||
if (!req.session.chatHistory) {
|
||||
req.session.chatHistory = [];
|
||||
if (!content) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'No content provided'
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize or get persistent chat session ID for this user
|
||||
if (!req.session.chatSessionId) {
|
||||
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Prepare the request payload for Flowise with sessionId and chatId
|
||||
const flowisePayload = {
|
||||
question: message,
|
||||
sessionId: req.session.chatSessionId
|
||||
};
|
||||
|
||||
// Add chatId if we have one from previous conversations
|
||||
if (req.session.chatId) {
|
||||
flowisePayload.chatId = req.session.chatId;
|
||||
|
||||
// Process content based on display mode
|
||||
switch (displayMode) {
|
||||
case 'html':
|
||||
if (isMarkdownContent || autoDetect === false) {
|
||||
// Convert markdown to safe HTML
|
||||
renderedContent = 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// Save session explicitly since we modified it
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
console.error('Error saving chat session:', err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
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 || [];
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
history: chatHistory
|
||||
renderedContent: renderedContent,
|
||||
displayMode: displayMode,
|
||||
detectedFormat: detectedFormat,
|
||||
isMarkdownContent: isMarkdownContent
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting chat history:', error);
|
||||
console.error('Error rendering content:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get chat history',
|
||||
error: 'Failed to render content',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clear chat history
|
||||
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.status(500).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.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to clear chat history',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/dashboard', requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Load persistent files for this user
|
||||
const persistentFiles = await loadUserFiles(req.session.userId);
|
||||
|
||||
// Load revised files separately
|
||||
const revisedFiles = await loadRevisedFiles(req.session.userId);
|
||||
|
||||
// Merge with session files (in case there are newly uploaded files not yet saved)
|
||||
const sessionFiles = req.session.uploadedFiles || [];
|
||||
const allFiles = [...persistentFiles];
|
||||
|
||||
// Add any session files that aren't already in persistent storage
|
||||
sessionFiles.forEach(sessionFile => {
|
||||
if (!persistentFiles.find(f => f.id === sessionFile.id)) {
|
||||
allFiles.push(sessionFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Merge revised files from session
|
||||
const sessionRevisedFiles = req.session.revisedFiles || [];
|
||||
const allRevisedFiles = [...revisedFiles];
|
||||
sessionRevisedFiles.forEach(sessionFile => {
|
||||
if (!revisedFiles.find(f => f.id === sessionFile.id)) {
|
||||
allRevisedFiles.push(sessionFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Update session with merged files for current session use
|
||||
req.session.uploadedFiles = allFiles;
|
||||
req.session.revisedFiles = allRevisedFiles;
|
||||
|
||||
res.render('dashboard', {
|
||||
title: 'Dashboard - EduCat',
|
||||
files: allFiles,
|
||||
revisedFiles: allRevisedFiles
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard:', error);
|
||||
res.render('dashboard', {
|
||||
title: 'Dashboard - EduCat',
|
||||
files: req.session.uploadedFiles || [],
|
||||
revisedFiles: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// File management endpoints
|
||||
// Get file preview endpoint (simplified for dashboard)
|
||||
app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const fileId = req.params.fileId;
|
||||
@@ -1417,12 +1495,102 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
const file = files.find(f => f.id === fileId);
|
||||
|
||||
if (!file) {
|
||||
return res.status(404).json({ success: false, error: 'File not found' });
|
||||
// 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);
|
||||
|
||||
@@ -1435,7 +1603,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
|
||||
extractedText;
|
||||
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
@@ -1457,7 +1625,6 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
} else {
|
||||
// Failed to extract text, fall back to file type detection
|
||||
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
|
||||
const binaryFormats = ['.pdf', '.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'];
|
||||
|
||||
if (textFormats.includes(fileExtension)) {
|
||||
// Try reading as plain text (should have been handled by extraction, but fallback)
|
||||
@@ -1467,7 +1634,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
|
||||
fileContent;
|
||||
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
@@ -1479,7 +1646,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
} catch (readError) {
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
@@ -1494,14 +1661,213 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
}
|
||||
} else {
|
||||
// Binary format that couldn't be processed
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
originalName: file.originalName,
|
||||
size: file.size,
|
||||
uploadDate: file.uploadDate,
|
||||
content: 'Text extraction failed',
|
||||
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.`
|
||||
}
|
||||
@@ -2338,6 +2704,147 @@ app.listen(PORT, () => {
|
||||
console.log(`EduCat server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
// Helper function to convert markdown to safe HTML
|
||||
function markdownToSafeHtml(markdownText) {
|
||||
try {
|
||||
// Configure marked options for better security and features
|
||||
marked.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 = marked.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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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 {
|
||||
await ensureChatHistoryDirectory();
|
||||
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
|
||||
await fs.writeJSON(historyPath, chatHistory, { spaces: 2 });
|
||||
} catch (error) {
|
||||
console.error('Error saving chat history:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Load chat history from persistent storage
|
||||
async function loadChatHistory(userId) {
|
||||
try {
|
||||
await ensureChatHistoryDirectory();
|
||||
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
|
||||
|
||||
if (await fs.pathExists(historyPath)) {
|
||||
return await fs.readJSON(historyPath);
|
||||
} else {
|
||||
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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user