Compare commits

...

10 Commits

Author SHA1 Message Date
inubimambo
b9d7ff0e1d Fix chat history not persistent again 2025-07-12 14:45:30 +08:00
inubimambo
bf40149c0e Fix require() of ES Module /app/node_modules/marked/lib/marked.esm.js from /app/server.js not supported. 2025-07-12 14:18:37 +08:00
inubimambo
a08f767841 Add HTML & Markdown preview in dashboard and revise pages 2025-07-12 13:53:07 +08:00
inubimambo
51c3c6b577 Improve chat output and formatting 2025-07-12 13:11:17 +08:00
inubimambo
df1505bf48 Update README 2025-07-09 22:51:51 +08:00
inubimambo
10573072ff Fix: RAG can get corrupted upon uploading other file types 2025-07-09 21:42:08 +08:00
inubimambo
e6639cb37f Add favicon 2025-07-09 21:32:56 +08:00
inubimambo
bcd5a52995 Improve dashboard UI and some fixes 2025-07-08 23:14:31 +08:00
inubimambo
9644f62dc5 Improve text extraction in dashboard 2025-07-08 21:00:17 +08:00
inubimambo
a87484b0e7 Fix 'Review Answer' button in Quiz doing nothing & improvements to quiz 2025-07-08 20:46:24 +08:00
15 changed files with 4725 additions and 422 deletions

225
README.md
View File

@@ -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

1503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,19 @@
"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",
"express": "^4.19.2",
"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"
},
"devDependencies": {

View File

@@ -156,8 +156,72 @@ body {
}
}
.file-icon {
font-size: 1.2rem;
.dashboard-file-icon {
font-size: 1.25rem !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 50px !important;
width: 50px !important;
height: 50px !important;
flex-shrink: 0 !important;
}
.dashboard-file-icon i {
font-size: 1.25rem !important;
line-height: 1 !important;
margin: 0 !important;
padding: 0 !important;
vertical-align: middle !important;
}
/* Force proper icon rendering to prevent compression */
.revised-files-section .file-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 50% !important;
overflow: hidden;
position: relative;
}
.revised-files-section .file-icon::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background: inherit;
z-index: -1;
}
/* AI text icon styling */
.revised-files-section .ai-icon {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
color: white !important;
font-weight: bold !important;
font-size: 1.2rem !important;
letter-spacing: 1px !important;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) !important;
border: 2px solid rgba(255, 255, 255, 0.2) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
/* Ensure icon font doesn't get compressed */
.revised-files-section .fa-brain {
transform: none !important;
font-weight: 900 !important;
font-family: "Font Awesome 5 Free" !important;
vertical-align: middle !important;
display: inline-block !important;
font-style: normal !important;
font-variant: normal !important;
text-rendering: auto !important;
line-height: 1 !important;
margin: 0 !important;
padding: 0 !important;
}
.progress-bar-animated {
@@ -190,48 +254,201 @@ body {
margin-top: auto;
}
/* Responsive adjustments */
/* Revised Files Section Styling */
.revised-files-section .card {
transition: all 0.3s ease;
}
.revised-files-section .card:hover {
transform: translateY(-2px);
box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.15) !important;
}
.revised-files-section .file-icon {
min-width: 50px !important;
width: 50px !important;
height: 50px !important;
flex-shrink: 0 !important;
font-size: 1.25rem !important;
line-height: 1 !important;
}
.revised-files-section .file-icon i {
font-size: 1.25rem !important;
line-height: 1 !important;
}
.revised-files-section .text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.revised-files-section .min-w-0 {
min-width: 0;
}
.revised-files-section .card-body {
overflow: hidden;
}
.revised-files-section .btn-group {
width: 100%;
}
.revised-files-section .btn-group .btn {
flex: 1;
}
.revised-files-section .btn-group .btn:last-child {
flex: 0 0 auto;
min-width: 40px;
}
/* File name truncation for long names */
.file-name-truncate {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
}
/* Badge styling improvements */
.revised-files-section .badge {
font-size: 0.7rem !important;
padding: 0.25em 0.5em;
white-space: nowrap;
}
/* Button spacing and sizing improvements */
.revised-files-section .d-grid .btn {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Ensure cards have consistent height */
.revised-files-section .card {
height: 100%;
display: flex;
flex-direction: column;
}
.revised-files-section .card-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* Responsive adjustments for small screens */
@media (max-width: 768px) {
.hero-section {
min-height: 50vh;
text-align: center;
.revised-files-section .file-name-truncate {
max-width: 150px;
}
.hero-section .display-4 {
font-size: 2rem;
}
.message-content {
max-width: 85% !important;
}
.avatar {
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
}
.avatar i {
.revised-files-section .file-icon {
width: 45px !important;
height: 45px !important;
min-width: 45px !important;
font-size: 1rem !important;
}
.message-bubble {
padding: 0.75rem !important;
font-size: 0.9rem;
.revised-files-section .ai-icon {
font-size: 1rem !important;
letter-spacing: 0.5px !important;
}
.card-body {
.revised-files-section .file-icon i {
font-size: 1.1rem !important;
}
.revised-files-section .btn-group .btn {
font-size: 0.8rem;
padding: 0.375rem 0.5rem;
}
}
/* Mobile responsive file icons */
@media (max-width: 768px) {
.dashboard-file-icon {
min-width: 45px !important;
width: 45px !important;
height: 45px !important;
font-size: 1.1rem !important;
}
.dashboard-file-icon i {
font-size: 1.1rem !important;
}
}
/* Dashboard section spacing */
.dashboard-section {
margin-bottom: 2rem;
}
.dashboard-section h3 {
font-weight: 600;
color: var(--dark-color);
}
/* Enhanced visual separation between sections */
.ai-revised-section {
background: linear-gradient(135deg, rgba(40, 167, 69, 0.05) 0%, rgba(40, 167, 69, 0.02) 100%);
border-radius: 1rem;
padding: 1.5rem;
}
margin-top: 2rem;
}
#chat-container {
height: 400px !important;
/* Badge positioning fixes for dashboard cards */
.card .badge {
position: relative;
z-index: 1;
display: inline-flex !important;
align-items: center !important;
white-space: nowrap !important;
}
.card .text-end {
text-align: right !important;
position: relative;
z-index: 1;
}
/* Ensure card body contains all content properly */
.card-body {
position: relative;
overflow: hidden;
padding: 1rem !important;
}
}
.chat-message {
margin-bottom: 1rem;
}
/* Prevent badge overflow */
.card .d-flex .text-end {
flex-shrink: 0;
min-width: fit-content;
}
.card .d-flex .flex-grow-1 {
overflow: hidden;
min-width: 0;
}
/* Dashboard card container fixes */
.dashboard-section .card {
position: relative;
overflow: visible;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.375rem;
}
.dashboard-section .card-body {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
/* Loading spinner */
@@ -397,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;
}
}

38
public/favicon-32x32.svg Normal file
View File

@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#007bff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0056b3;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="16" cy="16" r="15" fill="url(#grad1)" stroke="#ffffff" stroke-width="1"/>
<!-- Cat ears -->
<polygon points="8,8 12,4 16,8" fill="#ffffff" opacity="0.9"/>
<polygon points="16,8 20,4 24,8" fill="#ffffff" opacity="0.9"/>
<!-- Cat face -->
<ellipse cx="16" cy="18" rx="8" ry="6" fill="#ffffff" opacity="0.9"/>
<!-- Eyes -->
<circle cx="13" cy="16" r="1.5" fill="#007bff"/>
<circle cx="19" cy="16" r="1.5" fill="#007bff"/>
<!-- Nose -->
<polygon points="16,18 15,19 17,19" fill="#007bff"/>
<!-- Mouth -->
<path d="M 16 19 Q 14 21 12 20" stroke="#007bff" stroke-width="1" fill="none" stroke-linecap="round"/>
<path d="M 16 19 Q 18 21 20 20" stroke="#007bff" stroke-width="1" fill="none" stroke-linecap="round"/>
<!-- Whiskers -->
<line x1="8" y1="17" x2="11" y2="17" stroke="#007bff" stroke-width="0.5"/>
<line x1="8" y1="19" x2="11" y2="19" stroke="#007bff" stroke-width="0.5"/>
<line x1="21" y1="17" x2="24" y2="17" stroke="#007bff" stroke-width="0.5"/>
<line x1="21" y1="19" x2="24" y2="19" stroke="#007bff" stroke-width="0.5"/>
<!-- Small AI indicator -->
<text x="16" y="26" text-anchor="middle" font-family="Arial, sans-serif" font-size="6" font-weight="bold" fill="#ffffff">AI</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

32
public/favicon.svg Normal file
View File

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#007bff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0056b3;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="8" cy="8" r="7.5" fill="url(#grad1)" stroke="#ffffff" stroke-width="0.5"/>
<!-- Cat ears -->
<polygon points="4,4 6,2 8,4" fill="#ffffff" opacity="0.9"/>
<polygon points="8,4 10,2 12,4" fill="#ffffff" opacity="0.9"/>
<!-- Cat face -->
<ellipse cx="8" cy="9" rx="4" ry="3" fill="#ffffff" opacity="0.9"/>
<!-- Eyes -->
<circle cx="6.5" cy="8" r="0.7" fill="#007bff"/>
<circle cx="9.5" cy="8" r="0.7" fill="#007bff"/>
<!-- Nose -->
<polygon points="8,9 7.5,9.5 8.5,9.5" fill="#007bff"/>
<!-- Mouth -->
<path d="M 8 9.5 Q 7 10.5 6 10" stroke="#007bff" stroke-width="0.5" fill="none" stroke-linecap="round"/>
<path d="M 8 9.5 Q 9 10.5 10 10" stroke="#007bff" stroke-width="0.5" fill="none" stroke-linecap="round"/>
<!-- Small AI indicator -->
<text x="8" y="13" text-anchor="middle" font-family="Arial, sans-serif" font-size="3" font-weight="bold" fill="#ffffff">AI</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

38
public/images/favicon.svg Normal file
View File

@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#007bff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0056b3;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="16" cy="16" r="15" fill="url(#grad1)" stroke="#ffffff" stroke-width="1"/>
<!-- Cat ears -->
<polygon points="8,8 12,4 16,8" fill="#ffffff" opacity="0.9"/>
<polygon points="16,8 20,4 24,8" fill="#ffffff" opacity="0.9"/>
<!-- Cat face -->
<ellipse cx="16" cy="18" rx="8" ry="6" fill="#ffffff" opacity="0.9"/>
<!-- Eyes -->
<circle cx="13" cy="16" r="1.5" fill="#007bff"/>
<circle cx="19" cy="16" r="1.5" fill="#007bff"/>
<!-- Nose -->
<polygon points="16,18 15,19 17,19" fill="#007bff"/>
<!-- Mouth -->
<path d="M 16 19 Q 14 21 12 20" stroke="#007bff" stroke-width="1" fill="none" stroke-linecap="round"/>
<path d="M 16 19 Q 18 21 20 20" stroke="#007bff" stroke-width="1" fill="none" stroke-linecap="round"/>
<!-- Whiskers -->
<line x1="8" y1="17" x2="11" y2="17" stroke="#007bff" stroke-width="0.5"/>
<line x1="8" y1="19" x2="11" y2="19" stroke="#007bff" stroke-width="0.5"/>
<line x1="21" y1="17" x2="24" y2="17" stroke="#007bff" stroke-width="0.5"/>
<line x1="21" y1="19" x2="24" y2="19" stroke="#007bff" stroke-width="0.5"/>
<!-- Small AI indicator -->
<text x="16" y="26" text-anchor="middle" font-family="Arial, sans-serif" font-size="6" font-weight="bold" fill="#ffffff">AI</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -53,9 +53,60 @@ function initializeFileUpload() {
function showFileInfo(file) {
if (fileName && fileSize && fileInfo) {
fileName.textContent = file.name;
fileSize.textContent = `(${formatFileSize(file.size)})`;
// Define allowed file types for client-side validation
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'
];
// Get file extension
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
const fileMimeType = file.type.toLowerCase();
// Check if file type is allowed
const isExtensionAllowed = allowedExtensions.includes(fileExtension);
const isMimeTypeAllowed = allowedMimeTypes.includes(fileMimeType) || fileMimeType === '';
if (!isExtensionAllowed) {
// Show error for invalid file type
fileInfo.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Invalid file type!</strong> "${fileExtension}" files are not supported.
<br><small>Only document files (PDF, Word, Excel, text files) are allowed to prevent RAG corruption.</small>
</div>
`;
fileInfo.classList.remove('d-none');
uploadBtn.disabled = true;
return;
}
// Show valid file info
fileInfo.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-file me-2"></i>
<span>${file.name}</span>
<span class="text-muted ms-2">(${formatFileSize(file.size)})</span>
<div class="mt-2">
<small class="text-success">
<i class="fas fa-check-circle me-1"></i>
File type supported for AI processing
</small>
</div>
</div>
`;
fileInfo.classList.remove('d-none');
uploadBtn.disabled = false;
}
}

1603
server.js

File diff suppressed because it is too large Load Diff

View File

@@ -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 = `
<div class="d-flex align-items-start">
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3 flex-shrink-0" style="width: 45px; height: 45px; min-width: 45px;">
@@ -101,7 +111,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
<div class="message-content flex-grow-1">
<div class="message-bubble bg-light p-3 rounded-3 shadow-sm border">
<p class="mb-0">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?</p>
<div class="message-text">${formattedWelcome}</div>
</div>
<small class="text-muted d-block mt-1">Just now</small>
</div>
@@ -125,18 +135,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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
}
// For bot messages, apply rich formatting
let formatted = text
// Escape HTML first
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Convert markdown-style formatting
// Bold text **text** or __text__
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
formatted = formatted.replace(/__(.*?)__/g, '<strong>$1</strong>');
// Italic text *text* or _text_
formatted = formatted.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
formatted = formatted.replace(/(?<!_)_([^_]+)_(?!_)/g, '<em>$1</em>');
// Code blocks ```code```
formatted = formatted.replace(/```([\s\S]*?)```/g, '<pre class="code-block"><code>$1</code></pre>');
// Inline code `code`
formatted = formatted.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
// Headers
formatted = formatted.replace(/^### (.*$)/gm, '<h5 class="mt-3 mb-2">$1</h5>');
formatted = formatted.replace(/^## (.*$)/gm, '<h4 class="mt-3 mb-2">$1</h4>');
formatted = formatted.replace(/^# (.*$)/gm, '<h3 class="mt-3 mb-2">$1</h3>');
// Convert bullet points and numbered lists
// Handle bullet points: - item, * item, • item
formatted = formatted.replace(/^[\s]*[-*•]\s+(.+)$/gm, '<li class="bullet-item">$1</li>');
// Handle numbered lists: 1. item, 2. item, etc.
formatted = formatted.replace(/^[\s]*\d+\.\s+(.+)$/gm, '<li class="numbered-item">$1</li>');
// Wrap consecutive list items in proper list containers
formatted = formatted.replace(/(<li class="bullet-item">.*?<\/li>)(?:\s*<li class="bullet-item">.*?<\/li>)*/gs, function(match) {
return '<ul class="formatted-list">' + match + '</ul>';
});
formatted = formatted.replace(/(<li class="numbered-item">.*?<\/li>)(?:\s*<li class="numbered-item">.*?<\/li>)*/gs, function(match) {
return '<ol class="formatted-list">' + match.replace(/class="numbered-item"/g, 'class="list-item"') + '</ol>';
});
// Convert line breaks to <br> but not inside <pre> or list tags
let parts = formatted.split(/(<pre[\s\S]*?<\/pre>|<ul[\s\S]*?<\/ul>|<ol[\s\S]*?<\/ol>)/);
for (let i = 0; i < parts.length; i += 2) {
parts[i] = parts[i].replace(/\n\s*\n/g, '<br><br>').replace(/\n/g, '<br>');
}
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 = `
<div class="d-flex align-items-start justify-content-end">
<div class="message-content me-3 flex-grow-1" style="max-width: 70%;">
<div class="message-bubble bg-primary text-white p-3 rounded-3 shadow-sm">
<p class="mb-0">${message}</p>
<div class="message-text">${formattedMessage}</div>
</div>
<small class="text-muted d-block text-end mt-1">${time}</small>
</div>
@@ -153,7 +226,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
<div class="message-content flex-grow-1" style="max-width: 70%;">
<div class="message-bubble bg-light p-3 rounded-3 shadow-sm border">
<p class="mb-0">${message}</p>
<div class="message-text">${formattedMessage}</div>
</div>
<small class="text-muted d-block mt-1">${time}</small>
</div>

View File

@@ -31,7 +31,7 @@
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="file-icon bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px;">
<div class="dashboard-file-icon bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3">
<i class="fas fa-file-alt"></i>
</div>
<div class="flex-grow-1">
@@ -109,6 +109,73 @@
<% }); %>
</div>
<% } %>
<!-- Revised Files Section -->
<% if (typeof revisedFiles !== 'undefined' && revisedFiles.length > 0) { %>
<div class="ai-revised-section revised-files-section">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3><i class="fas fa-brain me-2 text-success"></i>AI-Revised Notes</h3>
<small class="text-muted bg-white px-2 py-1 rounded-pill">
<%= revisedFiles.length %> revised file<%= revisedFiles.length !== 1 ? 's' : '' %>
</small>
</div>
<div class="row">
<% revisedFiles.forEach(function(file, index) { %>
<div class="col-md-6 col-lg-4 mb-4">
<div class="card border-0 shadow-sm h-100 border-start border-4 border-success">
<div class="card-body p-3">
<div class="d-flex align-items-start mb-3">
<div class="file-icon bg-success text-white rounded-circle d-flex align-items-center justify-content-center me-3 ai-icon" style="width: 50px; height: 50px; min-width: 50px; font-size: 1.2rem; font-weight: bold; letter-spacing: 1px;">
AI
</div>
<div class="flex-grow-1 min-w-0">
<h6 class="mb-1 file-name-truncate" title="<%= file.originalName %>">
<%= file.originalName %>
</h6>
<div class="d-flex flex-wrap align-items-center gap-1 mb-1">
<small class="text-muted"><%= Math.round(file.size / 1024) %> KB</small>
<span class="badge bg-success text-white">
<%= file.revisionType.charAt(0).toUpperCase() + file.revisionType.slice(1) %>
</span>
</div>
</div>
</div>
<div class="mb-3">
<div class="small text-muted mb-1">
<i class="fas fa-calendar me-1"></i>
<%= new Date(file.uploadDate).toLocaleDateString() %>
</div>
<% if (file.originalFileName) { %>
<div class="small text-muted file-name-truncate" title="From: <%= file.originalFileName %>">
<i class="fas fa-file-alt me-1"></i>
From: <%= file.originalFileName %>
</div>
<% } %>
</div>
<div class="mt-auto">
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="previewRevisedFile('<%= file.id %>')">
<i class="fas fa-eye me-1"></i>Preview
</button>
<div class="btn-group w-100" role="group">
<a href="/uploads/revised-notes/<%= file.filename %>" download class="btn btn-success btn-sm">
<i class="fas fa-download me-1"></i>Download
</a>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteRevisedFile('<%= file.id %>')" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<% } %>
</div>
</div>
</div>
@@ -181,10 +248,8 @@ function previewFile(fileId) {
const file = result.file;
let content = '';
// Check file type and format content accordingly
const fileExtension = file.originalName.split('.').pop().toLowerCase();
if (['txt', 'md', 'json', 'js', 'html', 'css', 'py', 'java', 'cpp', 'c'].includes(fileExtension)) {
// Handle different preview types
if (file.previewType === 'text') {
// Text-based files - show with syntax highlighting
content = `
<div class="mb-3">
@@ -198,8 +263,35 @@ function previewFile(fileId) {
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"><code>${escapeHtml(file.content)}</code></pre>
</div>
`;
} else if (['pdf', 'doc', 'docx'].includes(fileExtension)) {
// Document files - show basic info and content preview
} else if (file.previewType === 'extracted-text') {
// Successfully extracted text from document
const extractionInfo = file.extractionInfo || {};
const infoText = [];
if (extractionInfo.pages) infoText.push(`${extractionInfo.pages} pages`);
if (extractionInfo.sheets) infoText.push(`${extractionInfo.sheets} sheets`);
if (extractionInfo.totalLength) infoText.push(`${extractionInfo.totalLength} characters extracted`);
content = `
<div class="mb-3">
<h6><i class="fas fa-file-word me-2"></i>${file.originalName}</h6>
<small class="text-muted">
Size: ${Math.round(file.size / 1024)} KB |
Uploaded: ${new Date(file.uploadDate).toLocaleDateString()}
${infoText.length > 0 ? ' | ' + infoText.join(', ') : ''}
</small>
</div>
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>
${file.message || 'Text successfully extracted from document'}
</div>
<div class="border rounded p-3" style="background-color: #f8f9fa; max-height: 400px; overflow-y: auto;">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(file.content)}</pre>
</div>
${extractionInfo.truncated ? '<small class="text-muted mt-2 d-block"><i class="fas fa-info-circle me-1"></i>Full document content is available for AI processing</small>' : ''}
`;
} else if (file.previewType === 'binary') {
// Binary files - show info message
content = `
<div class="mb-3">
<h6><i class="fas fa-file-pdf me-2"></i>${file.originalName}</h6>
@@ -210,14 +302,47 @@ function previewFile(fileId) {
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Document preview: First few lines of extracted text
${file.message || 'This is a binary file that has been processed for AI use.'}
</div>
${file.content && file.content !== 'File preview not available' ? `
<div class="border rounded p-3" style="background-color: #f8f9fa; max-height: 400px; overflow-y: auto;">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(file.content.substring(0, 1000))}${file.content.length > 1000 ? '...' : ''}</pre>
<small class="text-muted">Extracted content preview:</small>
<pre style="margin: 0; margin-top: 10px; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(file.content)}</pre>
</div>
` : ''}
`;
} else if (file.previewType === 'extraction-failed') {
// Failed to extract text
content = `
<div class="mb-3">
<h6><i class="fas fa-file-exclamation me-2"></i>${file.originalName}</h6>
<small class="text-muted">
Size: ${Math.round(file.size / 1024)} KB |
Uploaded: ${new Date(file.uploadDate).toLocaleDateString()}
</small>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
${file.message || 'Could not extract text from this file.'}
</div>
`;
} else if (file.previewType === 'error') {
// Error reading file
content = `
<div class="mb-3">
<h6><i class="fas fa-file-times me-2"></i>${file.originalName}</h6>
<small class="text-muted">
Size: ${Math.round(file.size / 1024)} KB |
Uploaded: ${new Date(file.uploadDate).toLocaleDateString()}
</small>
</div>
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
${file.message || 'Error reading file.'}
</div>
`;
} else {
// Other files - show basic info
// Fallback for unknown preview types
content = `
<div class="mb-3">
<h6><i class="fas fa-file me-2"></i>${file.originalName}</h6>
@@ -748,6 +873,148 @@ document.addEventListener('DOMContentLoaded', function() {
}, 10000);
}
});
// Preview revised file function
async function previewRevisedFile(fileId) {
try {
// Get file information by making an API call instead of using template data
const response = await fetch(`/api/revised-files/${fileId}/info`);
if (!response.ok) {
throw new Error('Failed to get file info');
}
const fileInfo = await response.json();
if (!fileInfo.success) {
throw new Error(fileInfo.error || 'Failed to get file info');
}
const file = fileInfo.file;
// 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 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 = `
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="alert alert-info mb-0 flex-grow-1 me-3">
<i class="fas fa-brain me-2"></i>
<strong>AI-Revised Content</strong> • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'}
</div>
<div class="btn-group btn-group-sm" role="group" id="preview-display-mode">
<input type="radio" class="btn-check" name="previewDisplayMode" id="preview-mode-markdown" value="markdown" checked>
<label class="btn btn-outline-secondary" for="preview-mode-markdown" title="Show as Markdown">
<i class="fab fa-markdown"></i>
</label>
<input type="radio" class="btn-check" name="previewDisplayMode" id="preview-mode-html" value="html">
<label class="btn btn-outline-secondary" for="preview-mode-html" title="Render as HTML">
<i class="fas fa-code"></i>
</label>
</div>
</div>
<div id="preview-content-container" class="border p-3 bg-light rounded" style="max-height: 400px; overflow-y: auto;">
<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(content)}</pre>
</div>
`;
// 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 = `<div class="rendered-markdown">${renderResult.renderedContent}</div>`;
if (renderResult.isMarkdownContent) {
htmlContent = `
<div class="alert alert-info alert-sm mb-2">
<small><i class="fas fa-info-circle me-1"></i>Markdown formatting detected and rendered</small>
</div>
${htmlContent}
`;
}
contentContainer.innerHTML = htmlContent;
contentContainer.style.backgroundColor = '#ffffff';
} else {
contentContainer.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(renderResult.renderedContent)}</pre>`;
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 = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(content)}</pre>`;
contentContainer.style.backgroundColor = '#f8f9fa';
}
}
modeMarkdown.addEventListener('change', updatePreviewMode);
modeHtml.addEventListener('change', updatePreviewMode);
modal.show();
} catch (error) {
console.error('Error previewing revised file:', error);
alert('Failed to preview file: ' + error.message);
}
}
// Delete revised file function
async function deleteRevisedFile(fileId) {
if (!confirm('Are you sure you want to delete this revised file? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/revised-files/${fileId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
// Reload the page to update the file list
location.reload();
} else {
alert('Error deleting file: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting revised file:', error);
alert('Network error: ' + error.message);
}
}
</script>
<%- include('partials/footer') %>

View File

@@ -4,6 +4,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/svg+xml" sizes="32x32" href="/favicon-32x32.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/images/favicon.svg">
<meta name="theme-color" content="#007bff">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">

View File

@@ -143,7 +143,7 @@
<!-- Results Container -->
<div id="results-container" class="d-none">
<div class="row">
<div class="col-lg-8">
<div class="col-lg-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-info text-white">
<h4 class="mb-0"><i class="fas fa-chart-line me-2"></i>Quiz Results</h4>
@@ -166,15 +166,6 @@
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5><i class="fas fa-trophy me-2"></i>Performance</h5>
<div id="performance-chart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -339,6 +330,14 @@ document.addEventListener('DOMContentLoaded', function() {
resetQuiz();
});
}
// Add event listener for review button
const reviewBtn = document.getElementById('review-btn');
if (reviewBtn) {
reviewBtn.addEventListener('click', () => {
reviewAnswers();
});
}
}
function displayQuestion(index) {
@@ -378,8 +377,8 @@ document.addEventListener('DOMContentLoaded', function() {
`;
});
html += '</div>';
} else if (question.correct === 'True' || question.correct === 'False') {
// True/False
} else if ((question.correct === 'True' || question.correct === 'False') && !question.answer) {
// True/False (only if it's not a short-answer question)
html += `
<div class="mb-3">
<div class="form-check mb-2">
@@ -556,6 +555,9 @@ document.addEventListener('DOMContentLoaded', function() {
}
function displayResults(results) {
// Store results globally for review functionality
window.lastQuizResults = results;
quizContainer.classList.add('d-none');
resultsContainer.classList.remove('d-none');
@@ -636,6 +638,256 @@ document.addEventListener('DOMContentLoaded', function() {
quizForm.reset();
}
function reviewAnswers() {
// Switch from results view back to quiz view for review
resultsContainer.classList.add('d-none');
quizContainer.classList.remove('d-none');
// Set to review mode and start from first question
currentQuestion = 0;
displayQuestionInReviewMode(currentQuestion);
displayOverviewInReviewMode();
// Update navigation for review mode
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
// Remove existing event listeners by cloning elements
if (prevBtn) {
const newPrevBtn = prevBtn.cloneNode(true);
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
newPrevBtn.onclick = () => {
if (currentQuestion > 0) {
currentQuestion--;
displayQuestionInReviewMode(currentQuestion);
}
};
}
if (nextBtn) {
const newNextBtn = nextBtn.cloneNode(true);
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
newNextBtn.onclick = () => {
if (currentQuestion < currentQuiz.length - 1) {
currentQuestion++;
displayQuestionInReviewMode(currentQuestion);
}
};
}
// Hide submit button completely in review mode
if (submitBtn) {
submitBtn.classList.add('d-none');
}
// Add a "Back to Results" button
const cardBody = submitBtn.parentElement;
let backToResultsBtn = document.getElementById('back-to-results-btn');
if (!backToResultsBtn) {
backToResultsBtn = document.createElement('button');
backToResultsBtn.id = 'back-to-results-btn';
backToResultsBtn.className = 'btn btn-info';
backToResultsBtn.innerHTML = '<i class="fas fa-arrow-left me-2"></i>Back to Results';
backToResultsBtn.onclick = () => {
quizContainer.classList.add('d-none');
resultsContainer.classList.remove('d-none');
// Remove the back button
backToResultsBtn.remove();
};
cardBody.appendChild(backToResultsBtn);
}
}
function displayQuestionInReviewMode(index) {
if (!currentQuiz || index >= currentQuiz.length) {
console.error('Invalid question index or quiz not loaded');
return;
}
const question = currentQuiz[index];
const questionsDiv = document.getElementById('quiz-questions');
if (!questionsDiv) {
console.error('Questions div not found!');
return;
}
// Get the quiz results from the last submission
const resultForThisQuestion = window.lastQuizResults ?
window.lastQuizResults.results[index] : null;
let html = `
<div class="mb-4">
<h5 class="mb-3">
Question ${index + 1}
${resultForThisQuestion ?
`<span class="badge ${resultForThisQuestion.isCorrect ? 'bg-success' : 'bg-danger'} ms-2">
${resultForThisQuestion.isCorrect ? 'Correct' : 'Incorrect'}
</span>` : ''
}
</h5>
<p class="lead">${escapeHtml(question.question)}</p>
</div>
`;
if (question.options) {
// Multiple choice - show options with user's answer and correct answer highlighted
html += '<div class="mb-3">';
question.options.forEach((option, i) => {
const optionLetter = option.charAt(0);
const userSelected = userAnswers[index] === optionLetter;
const isCorrect = question.correct === optionLetter;
let className = 'form-check mb-2';
let labelClass = 'form-check-label';
if (userSelected && isCorrect) {
className += ' bg-success bg-opacity-10 border border-success rounded p-2';
labelClass += ' text-success fw-bold';
} else if (userSelected && !isCorrect) {
className += ' bg-danger bg-opacity-10 border border-danger rounded p-2';
labelClass += ' text-danger fw-bold';
} else if (!userSelected && isCorrect) {
className += ' bg-warning bg-opacity-10 border border-warning rounded p-2';
labelClass += ' text-warning fw-bold';
}
html += `
<div class="${className}">
<input class="form-check-input" type="radio" disabled
${userSelected ? 'checked' : ''}>
<label class="${labelClass}">
${escapeHtml(option)}
${isCorrect ? ' ✓ (Correct answer)' : ''}
</label>
</div>
`;
});
html += '</div>';
} else if ((question.correct === 'True' || question.correct === 'False') && !question.answer) {
// True/False (only if it's not a short-answer question)
const userSelected = userAnswers[index];
const correctAnswer = question.correct;
['True', 'False'].forEach(option => {
const userSelectedThis = userSelected === option;
const isCorrect = correctAnswer === option;
let className = 'form-check mb-2';
let labelClass = 'form-check-label';
if (userSelectedThis && isCorrect) {
className += ' bg-success bg-opacity-10 border border-success rounded p-2';
labelClass += ' text-success fw-bold';
} else if (userSelectedThis && !isCorrect) {
className += ' bg-danger bg-opacity-10 border border-danger rounded p-2';
labelClass += ' text-danger fw-bold';
} else if (!userSelectedThis && isCorrect) {
className += ' bg-warning bg-opacity-10 border border-warning rounded p-2';
labelClass += ' text-warning fw-bold';
}
html += `
<div class="${className}">
<input class="form-check-input" type="radio" disabled
${userSelectedThis ? 'checked' : ''}>
<label class="${labelClass}">
${option}
${isCorrect ? ' ✓ (Correct answer)' : ''}
</label>
</div>
`;
});
} else {
// Short answer
const userAnswer = userAnswers[index] || '';
const correctAnswer = question.answer || question.correct || '';
html += `
<div class="mb-3">
<label class="form-label fw-bold">Your Answer:</label>
<div class="p-3 bg-light border rounded">
${userAnswer ? escapeHtml(userAnswer) : '<em>No answer provided</em>'}
</div>
<label class="form-label fw-bold mt-3 text-success">Correct Answer:</label>
<div class="p-3 bg-success bg-opacity-10 border border-success rounded">
${escapeHtml(correctAnswer)}
</div>
</div>
`;
}
// Add explanation if available
if (resultForThisQuestion && resultForThisQuestion.explanation) {
html += `
<div class="mt-3 p-3 bg-info bg-opacity-10 border border-info rounded">
<strong>Explanation:</strong>
<p class="mb-0 mt-2">${escapeHtml(resultForThisQuestion.explanation)}</p>
</div>
`;
}
questionsDiv.innerHTML = html;
// Update progress
const progressEl = document.getElementById('quiz-progress');
if (progressEl) {
progressEl.textContent = `Review: Question ${index + 1} of ${currentQuiz.length}`;
}
// Update navigation buttons for review mode
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
if (prevBtn) prevBtn.disabled = index === 0;
if (nextBtn) {
nextBtn.disabled = index === currentQuiz.length - 1;
nextBtn.classList.remove('d-none');
}
// Keep submit button hidden in review mode
if (submitBtn) {
submitBtn.classList.add('d-none');
}
}
function displayOverviewInReviewMode() {
const overviewDiv = document.getElementById('quiz-overview');
let html = '<div class="row">';
currentQuiz.forEach((_, i) => {
const resultForQuestion = window.lastQuizResults ?
window.lastQuizResults.results[i] : null;
const isCurrent = i === currentQuestion;
let btnClass = 'btn btn-sm w-100 ';
if (isCurrent) {
btnClass += 'btn-primary';
} else if (resultForQuestion) {
btnClass += resultForQuestion.isCorrect ? 'btn-success' : 'btn-danger';
} else {
btnClass += 'btn-outline-secondary';
}
html += `
<div class="col-4 mb-2">
<button class="${btnClass}" onclick="goToQuestionInReview(${i})">
${i + 1}
</button>
</div>
`;
});
html += '</div>';
overviewDiv.innerHTML = html;
}
window.goToQuestionInReview = function(index) {
currentQuestion = index;
displayQuestionInReviewMode(currentQuestion);
};
});
</script>

View File

@@ -8,15 +8,47 @@
<h3 class="mb-0"><i class="fas fa-edit me-2"></i>Revise: <%= file.originalName %></h3>
</div>
<div class="card-body">
<% if (extractionError) { %>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<%= extractionError %>
</div>
<% } else if (extractionInfo) { %>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Text extracted using <%= extractionInfo.method %>
<% if (extractionInfo.pages) { %>
| <%= extractionInfo.pages %> pages
<% } %>
<% if (extractionInfo.sheets) { %>
| <%= extractionInfo.sheets %> sheets
<% } %>
| <%= extractionInfo.totalLength %> characters
</div>
<% } %>
<div class="row">
<div class="col-md-6">
<h5>Original Notes</h5>
<div class="border p-3 bg-light rounded" style="height: 400px; overflow-y: auto;">
<pre class="mb-0"><%= content %></pre>
<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;"><%= content %></pre>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5>AI-Revised Notes</h5>
<div class="btn-group btn-group-sm" role="group" id="display-mode-toggle" style="display: none;">
<input type="radio" class="btn-check" name="displayMode" id="mode-markdown" value="markdown" checked>
<label class="btn btn-outline-secondary" for="mode-markdown" title="Show as Markdown">
<i class="fab fa-markdown"></i>
</label>
<input type="radio" class="btn-check" name="displayMode" id="mode-html" value="html">
<label class="btn btn-outline-secondary" for="mode-html" title="Render as HTML">
<i class="fas fa-code"></i>
</label>
</div>
</div>
<div id="revised-content" class="border p-3 bg-white rounded" style="height: 400px; overflow-y: auto;">
<p class="text-muted text-center mt-5">Select a revision type and click "Revise" to see AI-enhanced notes here.</p>
</div>
@@ -58,7 +90,32 @@
<li><strong>Name:</strong> <%= file.originalName %></li>
<li><strong>Size:</strong> <%= Math.round(file.size / 1024) %> KB</li>
<li><strong>Uploaded:</strong> <%= new Date(file.uploadDate).toLocaleDateString() %></li>
<% if (extractionInfo) { %>
<li><strong>Extraction:</strong> <%= extractionInfo.method %></li>
<% if (extractionInfo.pages) { %>
<li><strong>Pages:</strong> <%= extractionInfo.pages %></li>
<% } %>
<% if (extractionInfo.sheets) { %>
<li><strong>Sheets:</strong> <%= extractionInfo.sheets %></li>
<% } %>
<li><strong>Characters:</strong> <%= extractionInfo.totalLength.toLocaleString() %></li>
<% } %>
</ul>
<% if (file.status) { %>
<div class="mt-3">
<strong>Processing Status:</strong>
<% if (file.status === 'processed') { %>
<span class="badge bg-success ms-2">Processed</span>
<% } else if (file.status === 'processing') { %>
<span class="badge bg-warning ms-2">Processing</span>
<% } else if (file.status === 'failed') { %>
<span class="badge bg-danger ms-2">Failed</span>
<% } else { %>
<span class="badge bg-secondary ms-2"><%= file.status %></span>
<% } %>
</div>
<% } %>
</div>
</div>
@@ -95,16 +152,83 @@ 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 = `<div class="rendered-markdown">${result.renderedContent}</div>`;
// 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 = '<small><i class="fas fa-info-circle me-1"></i>Markdown formatting detected and rendered</small>';
revisedContent.insertBefore(formatInfo, revisedContent.firstChild);
}
} else {
// Show as raw markdown/text
revisedContent.innerHTML = `<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(result.renderedContent)}</pre>`;
}
} else {
throw new Error(result.error || 'Failed to render content');
}
} catch (error) {
console.error('Error rendering content:', error);
// Fallback to escaped text
revisedContent.innerHTML = `<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(content)}</pre>`;
}
}
// Add event listeners for display mode toggle
modeMarkdown.addEventListener('change', updateDisplayMode);
modeHtml.addEventListener('change', updateDisplayMode);
reviseBtn.addEventListener('click', async function() {
const type = revisionType.value;
const content = `<%= content.replace(/"/g, '\\"').replace(/\n/g, '\\n') %>`;
console.log('Revise button clicked with type:', type);
console.log('File ID:', fileId);
reviseBtn.disabled = true;
revisionProgress.classList.remove('d-none');
revisedContent.innerHTML = '<p class="text-muted text-center mt-5">Processing...</p>';
displayModeToggle.style.display = 'none'; // Hide toggle during processing
revisedContent.innerHTML = '<div class="text-center mt-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Processing...</span></div><p class="mt-2">AI is processing your notes...</p></div>';
try {
console.log('Sending request to /api/revise');
@@ -115,34 +239,120 @@ document.addEventListener('DOMContentLoaded', function() {
},
body: JSON.stringify({
content: content,
revisionType: type
revisionType: type,
fileId: fileId
})
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Revision result:', result);
if (result.success) {
revisedContent.innerHTML = '<pre class="mb-0">' + result.revisedContent + '</pre>';
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 {
revisedContent.innerHTML = '<div class="alert alert-danger">Error: ' + (result.error || 'Unknown error') + '</div>';
revisedContent.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i><strong>Error:</strong> ' + escapeHtml(result.error || 'Unknown error occurred') + '</div>';
}
} catch (error) {
console.error('Revision error:', error);
revisedContent.innerHTML = '<div class="alert alert-danger">Error: ' + error.message + '</div>';
revisedContent.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i><strong>Network Error:</strong> ' + escapeHtml(error.message) + '<br><small>Please check your connection and try again.</small></div>';
} finally {
reviseBtn.disabled = false;
revisionProgress.classList.add('d-none');
}
});
// Save revised notes button
saveBtn.addEventListener('click', async function() {
if (!currentRevisedContent) {
alert('No revised content to save. Please revise the notes first.');
return;
}
saveBtn.disabled = true;
const originalText = saveBtn.innerHTML;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
const response = await fetch('/api/save-revised', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileId: fileId,
revisedContent: currentRevisedContent,
revisionType: currentRevisionType
})
});
const result = await response.json();
if (result.success) {
// Show success message
const successAlert = document.createElement('div');
successAlert.className = 'alert alert-success alert-dismissible fade show mt-3';
successAlert.innerHTML = `
<i class="fas fa-check-circle me-2"></i>
<strong>Success!</strong> Revised notes saved as "${result.fileInfo.originalName}"
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.card-body').appendChild(successAlert);
// Auto-hide after 5 seconds
setTimeout(() => {
successAlert.remove();
}, 5000);
} else {
alert('Error saving revised notes: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Save error:', error);
alert('Network error while saving. Please try again.');
} finally {
saveBtn.disabled = false;
saveBtn.innerHTML = originalText;
}
});
// Download revised notes button
downloadBtn.addEventListener('click', function() {
if (!currentRevisedContent) {
alert('No revised content to download. Please revise the notes first.');
return;
}
// Create download URL with content and revision type as query parameters
const encodedContent = encodeURIComponent(currentRevisedContent);
const encodedRevisionType = encodeURIComponent(currentRevisionType);
const downloadUrl = `/api/download-revised/${fileId}?content=${encodedContent}&revisionType=${encodedRevisionType}`;
// Create temporary link and trigger download
const link = document.createElement('a');
link.href = downloadUrl;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
});
</script>

View File

@@ -14,12 +14,18 @@
<i class="fas fa-cloud-upload-alt fa-3x text-primary mb-3"></i>
<h5>Drag & Drop your files here</h5>
<p class="text-muted">or click to browse</p>
<input type="file" id="noteFile" name="noteFile" class="d-none" accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png,.gif">
<input type="file" id="noteFile" name="noteFile" class="d-none" accept=".pdf,.doc,.docx,.txt,.xlsx,.xls,.md,.json,.csv,.xml">
<button type="button" class="btn btn-primary" onclick="document.getElementById('noteFile').click()">
<i class="fas fa-folder-open me-2"></i>Choose File
</button>
</div>
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
<strong>Supported file types:</strong> PDF, Word (.doc, .docx), Excel (.xlsx, .xls), text files (.txt, .md, .json, .csv, .xml)
<br><small class="text-muted">Only text-extractable documents are allowed to ensure optimal AI processing and prevent RAG corruption.</small>
</div>
<div id="file-info" class="d-none mb-4">
<div class="alert alert-info">
<i class="fas fa-file me-2"></i>
@@ -46,25 +52,32 @@
</div>
<div class="row mt-5">
<div class="col-md-4">
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-file-pdf fa-2x text-danger mb-2"></i>
<h6>PDF Files</h6>
<p class="text-muted small">Upload PDF documents</p>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-file-word fa-2x text-primary mb-2"></i>
<h6>Word Documents</h6>
<p class="text-muted small">DOC & DOCX files</p>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-file-alt fa-2x text-success mb-2"></i>
<i class="fas fa-file-excel fa-2x text-success mb-2"></i>
<h6>Excel Files</h6>
<p class="text-muted small">XLSX & XLS files</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-file-alt fa-2x text-info mb-2"></i>
<h6>Text Files</h6>
<p class="text-muted small">Plain text documents</p>
<p class="text-muted small">TXT, MD, JSON, CSV, XML</p>
</div>
</div>
</div>