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/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 9a91bdc..4edc7e3 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -614,3 +614,357 @@ 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);
+}
+
+/* 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) {
+ .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;
+ }
+
+ .rendered-markdown table {
+ font-size: 0.875rem;
+ }
+
+ .rendered-markdown ul,
+ .rendered-markdown ol {
+ padding-left: 1.5rem;
+ }
+}
diff --git a/server.js b/server.js
index 61f5622..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 {
@@ -238,15 +247,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.'));
+ }
}
}
});
@@ -677,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'
@@ -967,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 {
@@ -1149,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 {
@@ -1188,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;
@@ -1389,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);
@@ -1407,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,
@@ -1429,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)
@@ -1439,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,
@@ -1451,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,
@@ -1466,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.`
}
@@ -2310,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 156b7c1..a9eed54 100644
--- a/views/chat.ejs
+++ b/views/chat.ejs
@@ -94,6 +94,16 @@ 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
+
+How can I assist you today?`;
+ const formattedWelcome = formatMessage(welcomeText, true);
+
messageDiv.innerHTML = `
$1');
+
+ // Inline code `code`
+ formatted = formatted.replace(/`([^`]+)`/g, '$1');
+
+ // Headers
+ formatted = formatted.replace(/^### (.*$)/gm, 'or list tags + let parts = formatted.split(/(|
${escapeHtml(content)}
+ ${escapeHtml(content)}
${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/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 @@
Select a revision type and click "Revise" to see AI-enhanced notes here.
${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 = 'AI is processing your notes...
' + 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 { 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 @@
or click to browse
- +Upload PDF documents
DOC & DOCX files
XLSX & XLS files
+Plain text documents
+TXT, MD, JSON, CSV, XML