From df1505bf483ce4b32c23f64bd81811152fd1ee50 Mon Sep 17 00:00:00 2001 From: inubimambo Date: Wed, 9 Jul 2025 22:51:51 +0800 Subject: [PATCH 1/3] Update README --- README.md | 225 +++++++++++++++++++++++++++++++++++--- server.js | 40 ++++++- views/partials/header.ejs | 7 ++ views/upload.ejs | 25 ++++- 4 files changed, 268 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 833eef0..0ec17da 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,50 @@ # EduCat - AI-Powered Note Revision Platform +EduCat is a modern web application that helps students improve their study notes using AI. Built with Node.js, Express, and EJS, it features secure file upload capabilities, AI-powered note revision, interactive quizzes, and an intelligent chatbot. + +## Features + +### ๐Ÿ” **Security & Authentication** +- Secure login and registration system +- Session-based authentication with flash messages +- Password hashing with bcrypt +- Strict file type whitelisting for secure uploads + +### ๐Ÿ“ **Document Processing** +- Upload notes in multiple formats (PDF, DOC, DOCX, XLSX, XLS, TXT, MD, JSON, CSV, XML) +- Secure document extraction with proper error handling +- File preview with extraction information and metadata +- Separate storage for original uploads and AI-revised notes + +### ๐Ÿค– **AI-Powered Features** +- **Note Revision**: Automatically improve, summarize, and generate study questions +- **Interactive Chatbot**: Chat with AI for study assistance +- **Quiz Generation**: Create interactive quizzes from uploaded documents +- **Smart Content Extraction**: Extract text from various document formats + +### ๐Ÿ“Š **Dashboard & Management** +- Modern dashboard to manage uploaded files +- Separate sections for original files and AI-revised notes +- File preview with extraction status and metadata +- Download and delete functionality for all file types + +### ๐ŸŽฏ **Quiz System** +- Generate quizzes from uploaded documents +- Multiple question types (multiple choice, true/false, short answer) +- Comprehensive quiz review with explanations +- Statistics tracking for quiz performance + +### ๐ŸŽจ **Modern UI/UX** +- Beautiful, responsive design with Bootstrap 5 +- Custom SVG favicon and branded interface +- Improved file icons and visual indicators +- Mobile-friendly responsive layout + +### ๐Ÿ”— **AI Integration** +- Connected to Flowise at https://flowise.suika.cc/ +- RAG (Retrieval-Augmented Generation) capabilities +- Secure API integration with proper error handling-Powered Note Revision Platform + EduCat is a modern web application that helps students improve their study notes using AI. Built with Node.js, Express, and EJS, it features file upload capabilities, AI-powered note revision, and an interactive chatbot. ## Features @@ -17,10 +62,15 @@ EduCat is a modern web application that helps students improve their study notes - **Backend**: Node.js, Express.js - **Frontend**: EJS templates, Bootstrap 5, Font Awesome - **Authentication**: bcrypt for password hashing, express-session -- **File Handling**: Multer for file uploads -- **AI Integration**: Flowise API integration +- **File Handling**: Multer for secure file uploads with type validation +- **Document Processing**: + - PDFParse for PDF extraction + - Mammoth for Word document processing + - ExcelJS for secure Excel file handling (replaced vulnerable xlsx) +- **AI Integration**: Flowise API integration with RAG capabilities - **Session Management**: Express Session with flash messages -- **Styling**: Custom CSS with Bootstrap +- **Security**: File type whitelisting, secure extraction methods +- **Styling**: Custom CSS with Bootstrap and custom SVG favicon ## Installation @@ -35,6 +85,17 @@ EduCat is a modern web application that helps students improve their study notes npm install ``` + **Key Dependencies:** + - `express` - Web framework + - `ejs` - Template engine + - `multer` - File upload handling + - `bcrypt` - Password hashing + - `express-session` - Session management + - `pdf-parse` - PDF text extraction + - `mammoth` - Word document processing + - `exceljs` - Secure Excel file handling + - `axios` - HTTP client for API calls + 3. **Configure environment variables**: - Copy `.env.example` to `.env` (if exists) or create a new `.env` file - Update the following variables: @@ -75,8 +136,12 @@ EduCat is a modern web application that helps students improve their study notes ### Uploading Notes 1. After logging in, click "Upload Notes" in the navigation 2. Drag and drop your file or click to browse -3. Select a file (PDF, DOC, TXT, or image) +3. Select a supported file type: + - **Documents**: PDF, DOC, DOCX + - **Spreadsheets**: XLSX, XLS + - **Text Files**: TXT, MD, JSON, CSV, XML 4. Click "Upload & Process" +5. View file preview with extraction information ### Revising Notes 1. Go to your Dashboard to see uploaded files @@ -86,11 +151,19 @@ EduCat is a modern web application that helps students improve their study notes - **Summarize**: Creates concise summaries - **Generate Questions**: Creates study questions 4. Click "Revise with AI" to process +5. Save and download revised notes from the "AI-Revised Notes" section + +### Quiz System +1. Upload a document to generate quizzes from +2. Navigate to the quiz section +3. Take interactive quizzes with multiple question types +4. Review answers with detailed explanations +5. Track your quiz performance and statistics ### Using the Chatbot 1. Navigate to the "Chat" section 2. Type your questions about study materials or academic topics -3. Get instant AI-powered responses +3. Get instant AI-powered responses using RAG technology ## Project Structure @@ -101,8 +174,10 @@ EduCat/ โ”‚ โ”‚ โ””โ”€โ”€ style.css โ”‚ โ”œโ”€โ”€ js/ โ”‚ โ”‚ โ””โ”€โ”€ main.js -โ”‚ โ””โ”€โ”€ images/ -โ”‚ โ””โ”€โ”€ logo.png +โ”‚ โ”œโ”€โ”€ images/ +โ”‚ โ”‚ โ””โ”€โ”€ favicon.svg +โ”‚ โ”œโ”€โ”€ favicon.svg +โ”‚ โ””โ”€โ”€ favicon-32x32.svg โ”œโ”€โ”€ views/ โ”‚ โ”œโ”€โ”€ partials/ โ”‚ โ”‚ โ”œโ”€โ”€ header.ejs @@ -112,8 +187,14 @@ EduCat/ โ”‚ โ”œโ”€โ”€ revise.ejs โ”‚ โ”œโ”€โ”€ chat.ejs โ”‚ โ”œโ”€โ”€ dashboard.ejs +โ”‚ โ”œโ”€โ”€ quiz.ejs โ”‚ โ””โ”€โ”€ error.ejs โ”œโ”€โ”€ uploads/ +โ”‚ โ””โ”€โ”€ revised-notes/ +โ”œโ”€โ”€ data/ +โ”‚ โ”œโ”€โ”€ user-files/ +โ”‚ โ”œโ”€โ”€ revised-files/ +โ”‚ โ””โ”€โ”€ quiz-results/ โ”œโ”€โ”€ server.js โ”œโ”€โ”€ package.json โ””โ”€โ”€ .env @@ -121,14 +202,38 @@ EduCat/ ## API Endpoints +### Authentication & Core Routes - `GET /` - Home page +- `GET /login` - Login page +- `POST /login` - Handle login +- `GET /register` - Registration page +- `POST /register` - Handle registration +- `GET /logout` - Logout user + +### File Management - `GET /upload` - File upload page -- `POST /upload` - Handle file uploads +- `POST /upload` - Handle file uploads with validation +- `GET /dashboard` - User dashboard with file management +- `GET /api/files/:fileId/preview` - File preview with extraction info +- `DELETE /api/files/:fileId` - Delete uploaded file + +### AI-Powered Features - `GET /revise/:fileId` - Note revision page - `POST /api/revise` - AI revision endpoint +- `POST /api/save-revised` - Save revised notes +- `GET /api/download-revised/:fileId` - Download revised notes +- `GET /api/revised-files/:fileId/info` - Get revised file info +- `DELETE /api/revised-files/:fileId` - Delete revised file + +### Quiz System +- `GET /quiz` - Quiz interface +- `POST /api/quiz/generate` - Generate quiz from document +- `POST /api/quiz/submit` - Submit quiz answers +- `GET /api/quiz/results` - Get quiz statistics + +### Chat Integration - `GET /chat` - Chat interface -- `POST /api/chat` - Chat API endpoint -- `GET /dashboard` - User dashboard +- `POST /api/chat` - Chat API endpoint with RAG support ## Configuration @@ -144,24 +249,37 @@ EduCat/ ### File Upload Settings - **Maximum file size**: 10MB -- **Allowed formats**: PDF, DOC, DOCX, TXT, JPG, JPEG, PNG, GIF -- **Upload directory**: `uploads/` +- **Allowed formats**: PDF, DOC, DOCX, XLSX, XLS, TXT, MD, JSON, CSV, XML +- **Upload directory**: `uploads/` (original files) +- **Revised notes directory**: `uploads/revised-notes/` +- **Security**: Strict file type whitelisting with MIME type validation +- **Processing**: Automatic text extraction with error handling ## Customization ### Styling - Edit `public/css/style.css` to customize the appearance - The design uses Bootstrap 5 with custom CSS variables +- Custom SVG favicon and file icons for better visual consistency +- Responsive design optimized for mobile and desktop ### AI Integration - Modify the Flowise API calls in `server.js` - Update prompts in the `/api/revise` endpoint - Customize chat responses in the `/api/chat` endpoint +- Configure RAG settings for document-based queries + +### Security +- File type restrictions configured in server.js and main.js +- MIME type validation for uploaded files +- Secure document extraction methods +- Session security with proper secret management ### Adding Features - Add new routes in `server.js` - Create corresponding EJS templates in `views/` - Add client-side JavaScript in `public/js/main.js` +- Update CSS in `public/css/style.css` ## Troubleshooting @@ -169,17 +287,38 @@ EduCat/ 1. **File upload fails**: - Check file size (max 10MB) - - Verify file format is supported - - Ensure `uploads/` directory exists + - Verify file format is supported (PDF, DOC, DOCX, XLSX, XLS, TXT, MD, JSON, CSV, XML) + - Ensure `uploads/` and `uploads/revised-notes/` directories exist + - Check file type validation in both client and server -2. **AI responses don't work**: +2. **Document extraction errors**: + - Verify document is not corrupted + - Check extraction status in file preview + - Ensure proper permissions for file access + - Review server logs for specific extraction errors + +3. **AI responses don't work**: - Verify Flowise API URL is correct - Check if your chatflow ID is valid - Ensure Flowise server is accessible + - Verify RAG configuration for document-based queries -3. **Session issues**: - - Verify SESSION_SECRET is set +4. **Quiz generation fails**: + - Ensure document has sufficient text content + - Check if document extraction was successful + - Verify AI service is properly connected + - Review quiz generation prompts + +5. **Session issues**: + - Verify SESSION_SECRET is set in .env - Check if sessions are properly configured + - Clear browser cookies and try again + +6. **Revised notes not saving**: + - Ensure `uploads/revised-notes/` directory exists + - Check file permissions + - Verify sufficient disk space + - Review server logs for save errors ### Development @@ -189,6 +328,58 @@ npm install -g nodemon npm run dev ``` +## Recent Updates & Security Improvements + +### Version 2.0 Security Enhancements +- **๐Ÿ”’ Enhanced Security**: Replaced vulnerable `xlsx` library with secure `exceljs` for Excel processing +- **๐Ÿ›ก๏ธ File Type Whitelisting**: Implemented strict file type validation to prevent malicious uploads +- **๐Ÿ” MIME Type Validation**: Added comprehensive file type checking on both client and server +- **๐Ÿ—‚๏ธ Secure Document Processing**: Improved extraction methods with proper error handling + +### UI/UX Improvements +- **๐ŸŽจ Custom Favicon**: Added custom SVG favicon for brand consistency +- **๐Ÿ“ฑ Responsive Design**: Enhanced mobile-friendly interface with improved layouts +- **๐Ÿ”ง File Icons**: Updated file type icons for better visual clarity +- **๐Ÿ“Š Dashboard Enhancements**: Separate sections for original and revised files +- **๐Ÿท๏ธ Badge System**: Improved status indicators and badge alignment + +### New Features +- **๐Ÿ’พ Revised Notes Management**: Save, download, and manage AI-revised notes separately +- **๐ŸŽฏ Enhanced Quiz System**: Fixed short-answer questions with explanations +- **๐Ÿ“ˆ Quiz Statistics**: Comprehensive tracking of quiz performance +- **๐Ÿ” File Preview**: Detailed extraction information and metadata display +- **๐Ÿ“ Persistent Storage**: Improved file tracking and session management + +### Performance & Reliability +- **โšก Optimized Extraction**: Faster and more reliable document processing +- **๐Ÿ”„ Error Handling**: Comprehensive error handling for all file operations +- **๐Ÿ’ช Robust API**: Improved API endpoints with better validation +- **๐Ÿงน Code Refactoring**: Cleaner, more maintainable codebase + +## Deployment & Production + +### Production Considerations +- Set strong `SESSION_SECRET` in production environment +- Configure proper file upload limits based on server capacity +- Set up proper logging and monitoring +- Implement rate limiting for API endpoints +- Configure HTTPS for secure file uploads +- Set up backup procedures for uploaded files and data + +### Environment Setup +- Ensure Node.js 14+ is installed +- Create proper directory structure with correct permissions +- Configure environment variables for production +- Set up reverse proxy (nginx/Apache) if needed +- Configure SSL certificates for HTTPS + +### Monitoring & Maintenance +- Monitor disk space for uploads directory +- Set up log rotation for application logs +- Regular security updates for dependencies +- Monitor API usage and performance +- Backup user data and quiz results regularly + ## Contributing 1. Fork the repository diff --git a/server.js b/server.js index 61f5622..c0c006e 100644 --- a/server.js +++ b/server.js @@ -238,15 +238,43 @@ const upload = multer({ fileSize: 10 * 1024 * 1024 // 10MB limit }, fileFilter: (req, file, cb) => { - // Allow text files, PDFs, and images - const allowedTypes = /jpeg|jpg|png|gif|pdf|txt|doc|docx/; - const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); - const mimetype = allowedTypes.test(file.mimetype); + // 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' + ]; - if (mimetype && extname) { + 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 { - cb(new Error('Only text files, PDFs, and images are allowed!')); + // 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.')); + } } } }); diff --git a/views/partials/header.ejs b/views/partials/header.ejs index 8563293..ac5b4dc 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -4,6 +4,13 @@ <%= title %> + + + + + + + diff --git a/views/upload.ejs b/views/upload.ejs index 14047d5..abfdde3 100644 --- a/views/upload.ejs +++ b/views/upload.ejs @@ -14,12 +14,18 @@
Drag & Drop your files here

or click to browse

- + +
+ + Supported file types: PDF, Word (.doc, .docx), Excel (.xlsx, .xls), text files (.txt, .md, .json, .csv, .xml) +
Only text-extractable documents are allowed to ensure optimal AI processing and prevent RAG corruption. +
+
@@ -46,25 +52,32 @@
-
+
PDF Files

Upload PDF documents

-
+
Word Documents

DOC & DOCX files

-
+
- + +
Excel Files
+

XLSX & XLS files

+
+
+
+
+
Text Files
-

Plain text documents

+

TXT, MD, JSON, CSV, XML

From 51c3c6b57737f87854b42ba0be9f5319154d126f Mon Sep 17 00:00:00 2001 From: inubimambo Date: Sat, 12 Jul 2025 13:11:17 +0800 Subject: [PATCH 2/3] Improve chat output and formatting --- public/css/style.css | 143 +++++++++++++++++++++++++++++++++++++++++++ views/chat.ejs | 85 ++++++++++++++++++++++++- 2 files changed, 225 insertions(+), 3 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 9a91bdc..bf733b4 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -614,3 +614,146 @@ body { border-radius: 0.75rem; overflow: hidden; } + +/* Enhanced Chat Message Formatting */ +.message-text { + line-height: 1.6; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.message-text h3, +.message-text h4, +.message-text h5 { + color: inherit; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.message-text h3 { + font-size: 1.1rem; + border-bottom: 1px solid rgba(0,0,0,0.1); + padding-bottom: 0.25rem; +} + +.message-text h4 { + font-size: 1.05rem; +} + +.message-text h5 { + font-size: 1rem; +} + +/* Code formatting */ +.message-text .code-block { + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 0.375rem; + padding: 0.75rem; + margin: 0.5rem 0; + font-family: 'Courier New', monospace; + font-size: 0.875rem; + white-space: pre-wrap; + overflow-x: auto; + max-width: 100%; +} + +.message-text .inline-code { + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 0.25rem; + padding: 0.125rem 0.25rem; + font-family: 'Courier New', monospace; + font-size: 0.875rem; + color: #d63384; +} + +/* List formatting */ +.message-text .formatted-list { + margin: 0.5rem 0; + padding-left: 1.5rem; +} + +.message-text .formatted-list li { + margin-bottom: 0.25rem; + line-height: 1.5; +} + +.message-text ul.formatted-list { + list-style-type: disc; +} + +.message-text ol.formatted-list { + list-style-type: decimal; +} + +.message-text .bullet-item, +.message-text .list-item { + display: list-item; +} + +/* Bold and italic text */ +.message-text strong { + font-weight: 600; +} + +.message-text em { + font-style: italic; +} + +/* Paragraph spacing */ +.message-text br + br { + display: block; + margin: 0.5rem 0; + content: ""; +} + +/* Special styling for bot messages */ +.bot-message .message-text .code-block { + background-color: #f1f3f4; + border-color: #dadce0; +} + +.bot-message .message-text .inline-code { + background-color: #f1f3f4; + border-color: #dadce0; + color: #1a73e8; +} + +/* Special styling for user messages */ +.user-message .message-text .code-block { + background-color: rgba(255,255,255,0.1); + border-color: rgba(255,255,255,0.2); + color: #ffffff; +} + +.user-message .message-text .inline-code { + background-color: rgba(255,255,255,0.1); + border-color: rgba(255,255,255,0.2); + color: #ffffff; +} + +.user-message .message-text h3, +.user-message .message-text h4, +.user-message .message-text h5 { + color: #ffffff; + border-color: rgba(255,255,255,0.3); +} + +/* Responsive adjustments for mobile */ +@media (max-width: 768px) { + .message-text .code-block { + font-size: 0.8rem; + padding: 0.5rem; + } + + .message-text .formatted-list { + padding-left: 1.2rem; + } + + .message-text h3, + .message-text h4, + .message-text h5 { + font-size: 0.95rem; + } +} diff --git a/views/chat.ejs b/views/chat.ejs index 156b7c1..9c4e1e2 100644 --- a/views/chat.ejs +++ b/views/chat.ejs @@ -94,6 +94,22 @@ document.addEventListener('DOMContentLoaded', function() { function addWelcomeMessage() { const messageDiv = document.createElement('div'); messageDiv.className = 'chat-message bot-message mb-3'; + const welcomeText = `Hello! I'm **EduCat AI**, your study assistant. I can help you with: + +โ€ข Questions about your notes and study materials +โ€ข Study techniques and academic strategies +โ€ข Explanations of complex concepts +โ€ข Creating study plans and schedules + +I support **rich formatting** in my responses including: +- **Bold text** and *italic text* +- \`Code snippets\` and code blocks +- Numbered lists and bullet points +- Headers and structured content + +How can I assist you today?`; + const formattedWelcome = formatMessage(welcomeText, true); + messageDiv.innerHTML = `
@@ -101,7 +117,7 @@ document.addEventListener('DOMContentLoaded', function() {
-

Hello! I'm EduCat AI, your study assistant. I can help you with questions about your notes, study techniques, and academic topics. How can I assist you today?

+
${formattedWelcome}
Just now
@@ -125,18 +141,81 @@ document.addEventListener('DOMContentLoaded', function() { sendToAI(message); } + function formatMessage(text, isBot = false) { + if (!isBot) { + // For user messages, just escape HTML and preserve basic formatting + return text.replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + } + + // For bot messages, apply rich formatting + let formatted = text + // Escape HTML first + .replace(/&/g, '&') + .replace(//g, '>'); + + // Convert markdown-style formatting + // Bold text **text** or __text__ + formatted = formatted.replace(/\*\*(.*?)\*\*/g, '$1'); + formatted = formatted.replace(/__(.*?)__/g, '$1'); + + // Italic text *text* or _text_ + formatted = formatted.replace(/(?$1'); + formatted = formatted.replace(/(?$1'); + + // Code blocks ```code``` + formatted = formatted.replace(/```([\s\S]*?)```/g, '
$1
'); + + // Inline code `code` + formatted = formatted.replace(/`([^`]+)`/g, '$1'); + + // Headers + formatted = formatted.replace(/^### (.*$)/gm, '
$1
'); + formatted = formatted.replace(/^## (.*$)/gm, '

$1

'); + formatted = formatted.replace(/^# (.*$)/gm, '

$1

'); + + // Convert bullet points and numbered lists + // Handle bullet points: - item, * item, โ€ข item + formatted = formatted.replace(/^[\s]*[-*โ€ข]\s+(.+)$/gm, '
  • $1
  • '); + + // Handle numbered lists: 1. item, 2. item, etc. + formatted = formatted.replace(/^[\s]*\d+\.\s+(.+)$/gm, '
  • $1
  • '); + + // Wrap consecutive list items in proper list containers + formatted = formatted.replace(/(
  • .*?<\/li>)(?:\s*
  • .*?<\/li>)*/gs, function(match) { + return '
      ' + match + '
    '; + }); + + formatted = formatted.replace(/(
  • .*?<\/li>)(?:\s*
  • .*?<\/li>)*/gs, function(match) { + return '
      ' + match.replace(/class="numbered-item"/g, 'class="list-item"') + '
    '; + }); + + // Convert line breaks to
    but not inside
     or list tags
    +        let parts = formatted.split(/(||)/);
    +        for (let i = 0; i < parts.length; i += 2) {
    +            parts[i] = parts[i].replace(/\n\s*\n/g, '

    ').replace(/\n/g, '
    '); + } + formatted = parts.join(''); + + return formatted; + } + function addMessageToChat(sender, message, withScroll = true) { const messageDiv = document.createElement('div'); messageDiv.className = `chat-message ${sender}-message mb-3`; const time = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + const formattedMessage = formatMessage(message, sender === 'bot'); if (sender === 'user') { messageDiv.innerHTML = `
    -

    ${message}

    +
    ${formattedMessage}
    ${time}
    @@ -153,7 +232,7 @@ document.addEventListener('DOMContentLoaded', function() {
    -

    ${message}

    +
    ${formattedMessage}
    ${time}
    From a08f7678415e228ea0c9d5f0e5c35238b9c52d66 Mon Sep 17 00:00:00 2001 From: inubimambo Date: Sat, 12 Jul 2025 13:53:07 +0800 Subject: [PATCH 3/3] Add HTML & Markdown preview in dashboard and revise pages --- package-lock.json | 575 ++++++++++++++++++++++++++++ package.json | 3 + public/css/style.css | 229 +++++++++++- server.js | 871 ++++++++++++++++++++++++++++++++++--------- views/chat.ejs | 6 - views/dashboard.ejs | 91 ++++- views/revise.ejs | 85 ++++- 7 files changed, 1653 insertions(+), 207 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b02546..0fdc099 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "body-parser": "^1.20.2", "connect-flash": "^0.1.1", "cors": "^2.8.5", + "dompurify": "^3.2.6", "dotenv": "^16.4.5", "ejs": "^3.1.10", "exceljs": "^4.4.0", @@ -21,7 +22,9 @@ "express-session": "^1.18.0", "form-data": "^4.0.3", "fs-extra": "^11.2.0", + "jsdom": "^26.1.0", "mammoth": "^1.9.1", + "marked": "^16.0.0", "multer": "^2.0.0", "pdf-parse": "^1.1.1", "uuid": "^10.0.0" @@ -30,6 +33,129 @@ "nodemon": "^3.0.1" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@fast-csv/format": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", @@ -85,6 +211,13 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -794,6 +927,66 @@ "node": ">= 10" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -809,6 +1002,12 @@ "ms": "2.0.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -858,6 +1057,15 @@ "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", "license": "BSD-2-Clause" }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -977,6 +1185,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1541,6 +1761,18 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1557,6 +1789,51 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1719,6 +1996,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1743,6 +2026,136 @@ "node": ">=10" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jsdom/node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1944,6 +2357,12 @@ "underscore": "^1.13.1" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1992,6 +2411,18 @@ "node": ">=12.0.0" } }, + "node_modules/marked": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.0.0.tgz", + "integrity": "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2307,6 +2738,12 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2370,6 +2807,18 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2467,6 +2916,15 @@ "dev": true, "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2588,6 +3046,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2871,6 +3335,12 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -2916,6 +3386,24 @@ "node": ">=10" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -2957,6 +3445,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -3119,12 +3619,57 @@ "node": ">= 0.8" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -3150,6 +3695,36 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", diff --git a/package.json b/package.json index 00b96f4..e79ba1c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "body-parser": "^1.20.2", "connect-flash": "^0.1.1", "cors": "^2.8.5", + "dompurify": "^3.2.6", "dotenv": "^16.4.5", "ejs": "^3.1.10", "exceljs": "^4.4.0", @@ -20,7 +21,9 @@ "express-session": "^1.18.0", "form-data": "^4.0.3", "fs-extra": "^11.2.0", + "jsdom": "^26.1.0", "mammoth": "^1.9.1", + "marked": "^16.0.0", "multer": "^2.0.0", "pdf-parse": "^1.1.1", "uuid": "^10.0.0" diff --git a/public/css/style.css b/public/css/style.css index bf733b4..4edc7e3 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -740,20 +740,231 @@ body { border-color: rgba(255,255,255,0.3); } -/* Responsive adjustments for mobile */ +/* Rendered Markdown Styles */ +.rendered-markdown { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #333; + max-width: 100%; + word-wrap: break-word; +} + +.rendered-markdown h1, +.rendered-markdown h2, +.rendered-markdown h3, +.rendered-markdown h4, +.rendered-markdown h5, +.rendered-markdown h6 { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-weight: 600; + line-height: 1.25; +} + +.rendered-markdown h1 { + font-size: 1.75rem; + border-bottom: 2px solid #e1e4e8; + padding-bottom: 0.5rem; +} + +.rendered-markdown h2 { + font-size: 1.5rem; + border-bottom: 1px solid #e1e4e8; + padding-bottom: 0.3rem; +} + +.rendered-markdown h3 { + font-size: 1.25rem; +} + +.rendered-markdown h4 { + font-size: 1.1rem; +} + +.rendered-markdown h5, +.rendered-markdown h6 { + font-size: 1rem; +} + +.rendered-markdown p { + margin-bottom: 1rem; +} + +.rendered-markdown strong, +.rendered-markdown b { + font-weight: 600; +} + +.rendered-markdown em, +.rendered-markdown i { + font-style: italic; +} + +.rendered-markdown ul, +.rendered-markdown ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +.rendered-markdown li { + margin-bottom: 0.25rem; +} + +.rendered-markdown li > p { + margin-bottom: 0.5rem; +} + +.rendered-markdown ul { + list-style-type: disc; +} + +.rendered-markdown ol { + list-style-type: decimal; +} + +.rendered-markdown blockquote { + margin: 1rem 0; + padding: 0.5rem 1rem; + border-left: 4px solid #dfe2e5; + background-color: #f6f8fa; + color: #6a737d; +} + +.rendered-markdown blockquote p:last-child { + margin-bottom: 0; +} + +.rendered-markdown pre { + background-color: #f6f8fa; + border-radius: 6px; + font-size: 0.875rem; + line-height: 1.45; + overflow: auto; + padding: 1rem; + margin-bottom: 1rem; + font-family: 'Courier New', Consolas, monospace; +} + +.rendered-markdown code { + background-color: rgba(27, 31, 35, 0.05); + border-radius: 3px; + font-size: 0.875rem; + margin: 0; + padding: 0.2em 0.4em; + font-family: 'Courier New', Consolas, monospace; +} + +.rendered-markdown pre code { + background-color: transparent; + border-radius: 0; + font-size: inherit; + margin: 0; + padding: 0; + word-break: normal; + white-space: pre; + word-wrap: normal; +} + +.rendered-markdown table { + border-collapse: collapse; + margin-bottom: 1rem; + width: 100%; +} + +.rendered-markdown table th, +.rendered-markdown table td { + border: 1px solid #dfe2e5; + padding: 6px 13px; + text-align: left; +} + +.rendered-markdown table th { + background-color: #f6f8fa; + font-weight: 600; +} + +.rendered-markdown table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.rendered-markdown a { + color: #0366d6; + text-decoration: none; +} + +.rendered-markdown a:hover { + text-decoration: underline; +} + +.rendered-markdown hr { + border: none; + border-top: 1px solid #e1e4e8; + height: 1px; + margin: 1.5rem 0; +} + +.rendered-markdown img { + max-width: 100%; + height: auto; + border-radius: 6px; + margin: 0.5rem 0; +} + +/* Display mode toggle styling */ +#display-mode-toggle .btn-outline-secondary { + border-color: #6c757d; + color: #6c757d; + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +#display-mode-toggle .btn-check:checked + .btn-outline-secondary { + background-color: #6c757d; + border-color: #6c757d; + color: white; +} + +#display-mode-toggle .btn-outline-secondary:hover { + background-color: #5c636a; + border-color: #565e64; + color: white; +} + +/* Alert for format detection */ +.alert-sm { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +/* Responsive adjustments */ @media (max-width: 768px) { - .message-text .code-block { + .rendered-markdown { + font-size: 0.9rem; + } + + .rendered-markdown h1 { + font-size: 1.5rem; + } + + .rendered-markdown h2 { + font-size: 1.3rem; + } + + .rendered-markdown h3 { + font-size: 1.1rem; + } + + .rendered-markdown pre { + padding: 0.75rem; font-size: 0.8rem; - padding: 0.5rem; } - .message-text .formatted-list { - padding-left: 1.2rem; + .rendered-markdown table { + font-size: 0.875rem; } - .message-text h3, - .message-text h4, - .message-text h5 { - font-size: 0.95rem; + .rendered-markdown ul, + .rendered-markdown ol { + padding-left: 1.5rem; } } diff --git a/server.js b/server.js index c0c006e..8cc2f07 100644 --- a/server.js +++ b/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, '
    '); + } + 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
    + 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); + diff --git a/views/chat.ejs b/views/chat.ejs index 9c4e1e2..a9eed54 100644 --- a/views/chat.ejs +++ b/views/chat.ejs @@ -101,12 +101,6 @@ document.addEventListener('DOMContentLoaded', function() { โ€ข Explanations of complex concepts โ€ข Creating study plans and schedules -I support **rich formatting** in my responses including: -- **Bold text** and *italic text* -- \`Code snippets\` and code blocks -- Numbered lists and bullet points -- Headers and structured content - How can I assist you today?`; const formattedWelcome = formatMessage(welcomeText, true); diff --git a/views/dashboard.ejs b/views/dashboard.ejs index adc90e9..64182d6 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -890,25 +890,100 @@ async function previewRevisedFile(fileId) { const file = fileInfo.file; - // Download the file content - const contentResponse = await fetch(`/uploads/revised-notes/${file.filename}`); + // Get file content using the new API endpoint + const contentResponse = await fetch(`/api/revised-files/${fileId}/content`); if (!contentResponse.ok) { throw new Error('Failed to load file content'); } - const content = await contentResponse.text(); + const contentResult = await contentResponse.json(); + if (!contentResult.success) { + throw new Error('Failed to load file content'); + } + + const content = contentResult.content; const modal = new bootstrap.Modal(document.getElementById('previewModal')); + // Create preview content with display mode toggle document.getElementById('preview-content').innerHTML = ` -
    - - AI-Revised Content โ€ข ${file.revisionType} โ€ข From: ${file.originalFileName || 'Unknown'} +
    +
    + + AI-Revised Content โ€ข ${file.revisionType} โ€ข From: ${file.originalFileName || 'Unknown'} +
    +
    + + + + + +
    -
    -
    ${escapeHtml(content)}
    +
    +
    ${escapeHtml(content)}
    `; + // Add event listeners for display mode toggle + const modeMarkdown = document.getElementById('preview-mode-markdown'); + const modeHtml = document.getElementById('preview-mode-html'); + const contentContainer = document.getElementById('preview-content-container'); + + async function updatePreviewMode() { + const selectedMode = document.querySelector('input[name="previewDisplayMode"]:checked').value; + + try { + const renderResponse = await fetch('/api/render-revised-content', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: content, + displayMode: selectedMode, + autoDetect: true + }) + }); + + const renderResult = await renderResponse.json(); + + if (renderResult.success) { + if (selectedMode === 'html') { + let htmlContent = `
    ${renderResult.renderedContent}
    `; + + if (renderResult.isMarkdownContent) { + htmlContent = ` +
    + Markdown formatting detected and rendered +
    + ${htmlContent} + `; + } + + contentContainer.innerHTML = htmlContent; + contentContainer.style.backgroundColor = '#ffffff'; + } else { + contentContainer.innerHTML = `
    ${escapeHtml(renderResult.renderedContent)}
    `; + contentContainer.style.backgroundColor = '#f8f9fa'; + } + } else { + throw new Error(renderResult.error || 'Failed to render content'); + } + } catch (error) { + console.error('Error rendering preview:', error); + // Fallback to escaped text + contentContainer.innerHTML = `
    ${escapeHtml(content)}
    `; + contentContainer.style.backgroundColor = '#f8f9fa'; + } + } + + modeMarkdown.addEventListener('change', updatePreviewMode); + modeHtml.addEventListener('change', updatePreviewMode); + modal.show(); } catch (error) { console.error('Error previewing revised file:', error); diff --git a/views/revise.ejs b/views/revise.ejs index a6c89d0..8cf67b5 100644 --- a/views/revise.ejs +++ b/views/revise.ejs @@ -35,7 +35,20 @@
    -
    AI-Revised Notes
    +
    +
    AI-Revised Notes
    + +

    Select a revision type and click "Revise" to see AI-enhanced notes here.

    @@ -139,11 +152,72 @@ document.addEventListener('DOMContentLoaded', function() { const revisionType = document.getElementById('revision-type'); const saveBtn = document.getElementById('save-btn'); const downloadBtn = document.getElementById('download-btn'); + const displayModeToggle = document.getElementById('display-mode-toggle'); + const modeMarkdown = document.getElementById('mode-markdown'); + const modeHtml = document.getElementById('mode-html'); const fileId = '<%= file.id %>'; const content = <%- JSON.stringify(content) %>; let currentRevisedContent = ''; let currentRevisionType = ''; + let currentDisplayMode = 'markdown'; + + // Handle display mode changes + function updateDisplayMode() { + if (!currentRevisedContent) return; + + const selectedMode = document.querySelector('input[name="displayMode"]:checked').value; + currentDisplayMode = selectedMode; + + renderRevisedContent(currentRevisedContent, selectedMode); + } + + // Render revised content based on display mode + async function renderRevisedContent(content, displayMode = 'markdown') { + try { + const response = await fetch('/api/render-revised-content', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: content, + displayMode: displayMode, + autoDetect: true + }) + }); + + const result = await response.json(); + + if (result.success) { + if (displayMode === 'html') { + // Render as HTML + revisedContent.innerHTML = `
    ${result.renderedContent}
    `; + + // Show format detection info if available + if (result.isMarkdownContent) { + const formatInfo = document.createElement('div'); + formatInfo.className = 'alert alert-info alert-sm mb-2'; + formatInfo.innerHTML = 'Markdown formatting detected and rendered'; + revisedContent.insertBefore(formatInfo, revisedContent.firstChild); + } + } else { + // Show as raw markdown/text + revisedContent.innerHTML = `
    ${escapeHtml(result.renderedContent)}
    `; + } + } else { + throw new Error(result.error || 'Failed to render content'); + } + } catch (error) { + console.error('Error rendering content:', error); + // Fallback to escaped text + revisedContent.innerHTML = `
    ${escapeHtml(content)}
    `; + } + } + + // Add event listeners for display mode toggle + modeMarkdown.addEventListener('change', updateDisplayMode); + modeHtml.addEventListener('change', updateDisplayMode); reviseBtn.addEventListener('click', async function() { const type = revisionType.value; @@ -153,6 +227,7 @@ document.addEventListener('DOMContentLoaded', function() { reviseBtn.disabled = true; revisionProgress.classList.remove('d-none'); + displayModeToggle.style.display = 'none'; // Hide toggle during processing revisedContent.innerHTML = '
    Processing...

    AI is processing your notes...

    '; try { @@ -175,9 +250,15 @@ document.addEventListener('DOMContentLoaded', function() { console.log('Revision result:', result); if (result.success) { - revisedContent.innerHTML = '
    ' + escapeHtml(result.revisedContent) + '
    '; currentRevisedContent = result.revisedContent; currentRevisionType = type; + + // Show display mode toggle + displayModeToggle.style.display = 'block'; + + // Render content based on current display mode + await renderRevisedContent(currentRevisedContent, currentDisplayMode); + saveBtn.disabled = false; downloadBtn.disabled = false; } else {