Add HTML & Markdown preview in dashboard and revise pages

This commit is contained in:
inubimambo
2025-07-12 13:53:07 +08:00
parent 51c3c6b577
commit a08f767841
7 changed files with 1653 additions and 207 deletions

871
server.js
View File

@@ -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 = {
'&': '&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 {
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);