EduCat with Flowise integration - complete implementation

This commit is contained in:
inubimambo
2025-07-05 23:37:31 +08:00
commit f5f5189fa0
21 changed files with 8184 additions and 0 deletions

85
.gitignore vendored Normal file
View File

@@ -0,0 +1,85 @@
# Environment variables
.env
.env.local
.env.production
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Uploads and user data
uploads/
data/
# Logs
logs/
*.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
tmp/
temp/

210
README.md Normal file
View File

@@ -0,0 +1,210 @@
# 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 file upload capabilities, AI-powered note revision, and an interactive chatbot.
## Features
- <20> **User Authentication**: Secure login and registration system
- <20>📁 **File Upload**: Upload notes in various formats (PDF, DOC, TXT, images)
- 🤖 **AI-Powered Revision**: Automatically improve, summarize, and generate study questions
- 💬 **Interactive Chatbot**: Chat with AI for study assistance
- 📊 **Dashboard**: Manage and track your uploaded notes
- 🎨 **Modern UI**: Beautiful, responsive design with Bootstrap
- 🔗 **Flowise Integration**: Connected to Flowise at https://flowise.suika.cc/
## Technologies Used
- **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
- **Session Management**: Express Session with flash messages
- **Styling**: Custom CSS with Bootstrap
## Installation
1. **Clone the repository**:
```bash
git clone <repository-url>
cd EduCat
```
2. **Install dependencies**:
```bash
npm install
```
3. **Configure environment variables**:
- Copy `.env.example` to `.env` (if exists) or create a new `.env` file
- Update the following variables:
```
PORT=3000
SESSION_SECRET=your-secret-key-here
FLOWISE_API_URL=https://flowise.suika.cc/api/v1/prediction
FLOWISE_CHATFLOW_ID=your-chatflow-id-here
```
4. **Set up your Flowise chatflow**:
- Go to https://flowise.suika.cc/
- Create or find your chatflow
- Copy the chatflow ID and update it in the `.env` file
5. **Start the application**:
```bash
npm start
```
For development with auto-reload:
```bash
npm run dev
```
6. **Access the application**:
- Open your browser and navigate to `http://localhost:3000`
## Usage
### Authentication
1. Visit `http://localhost:3000`
2. Click "Login" or "Register" to create an account
3. Use demo accounts:
- **Admin**: username `admin`, password `password`
- **Student**: username `student`, password `password`
### 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)
4. Click "Upload & Process"
### Revising Notes
1. Go to your Dashboard to see uploaded files
2. Click "Revise with AI" on any file
3. Choose revision type:
- **Improve & Enhance**: Makes notes more comprehensive
- **Summarize**: Creates concise summaries
- **Generate Questions**: Creates study questions
4. Click "Revise with AI" to process
### 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
## Project Structure
```
EduCat/
├── public/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── main.js
│ └── images/
│ └── logo.png
├── views/
│ ├── partials/
│ │ ├── header.ejs
│ │ └── footer.ejs
│ ├── index.ejs
│ ├── upload.ejs
│ ├── revise.ejs
│ ├── chat.ejs
│ ├── dashboard.ejs
│ └── error.ejs
├── uploads/
├── server.js
├── package.json
└── .env
```
## API Endpoints
- `GET /` - Home page
- `GET /upload` - File upload page
- `POST /upload` - Handle file uploads
- `GET /revise/:fileId` - Note revision page
- `POST /api/revise` - AI revision endpoint
- `GET /chat` - Chat interface
- `POST /api/chat` - Chat API endpoint
- `GET /dashboard` - User dashboard
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | `3000` |
| `SESSION_SECRET` | Session encryption key | `educat-secret-key` |
| `FLOWISE_API_URL` | Flowise API base URL | `https://flowise.suika.cc/api/v1/prediction` |
| `FLOWISE_CHATFLOW_ID` | Your Flowise chatflow ID | Required |
### File Upload Settings
- **Maximum file size**: 10MB
- **Allowed formats**: PDF, DOC, DOCX, TXT, JPG, JPEG, PNG, GIF
- **Upload directory**: `uploads/`
## Customization
### Styling
- Edit `public/css/style.css` to customize the appearance
- The design uses Bootstrap 5 with custom CSS variables
### 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
### Adding Features
- Add new routes in `server.js`
- Create corresponding EJS templates in `views/`
- Add client-side JavaScript in `public/js/main.js`
## Troubleshooting
### Common Issues
1. **File upload fails**:
- Check file size (max 10MB)
- Verify file format is supported
- Ensure `uploads/` directory exists
2. **AI responses don't work**:
- Verify Flowise API URL is correct
- Check if your chatflow ID is valid
- Ensure Flowise server is accessible
3. **Session issues**:
- Verify SESSION_SECRET is set
- Check if sessions are properly configured
### Development
To run in development mode with auto-reload:
```bash
npm install -g nodemon
npm run dev
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
This project is licensed under the MIT License.
## Support
For support or questions, please contact the EduCat development team.
---
**Made with ❤️ by the EduCat Team**

2293
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "educat-ai-notes",
"version": "1.0.0",
"description": "EduCat - AI-powered note revision platform for students",
"main": "server.js",
"scripts": {
"start": "node --no-deprecation server.js",
"dev": "nodemon --no-deprecation server.js"
},
"dependencies": {
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"connect-flash": "^0.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-session": "^1.18.0",
"form-data": "^4.0.3",
"fs-extra": "^11.2.0",
"multer": "^2.0.0",
"uuid": "^10.0.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": [
"education",
"ai",
"notes",
"revision",
"chatbot"
],
"author": "EduCat Team",
"license": "MIT"
}

399
public/css/style.css Normal file
View File

@@ -0,0 +1,399 @@
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--info-color: #17a2b8;
--warning-color: #ffc107;
--danger-color: #dc3545;
--light-color: #f8f9fa;
--dark-color: #343a40;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
}
.hero-section {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
min-height: 70vh;
display: flex;
align-items: center;
}
.bg-gradient-primary {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
}
.navbar-brand img {
border-radius: 50%;
}
.upload-dropzone {
transition: all 0.3s ease;
cursor: pointer;
}
.upload-dropzone:hover {
background-color: #e7f3ff;
border-color: #0056b3;
}
.upload-dropzone.dragover {
background-color: #e7f3ff;
border-color: #0056b3;
transform: scale(1.02);
}
.feature-icon {
font-size: 1.5rem;
}
.step-number {
font-size: 1.2rem;
font-weight: bold;
}
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.chat-message {
margin-bottom: 1.5rem;
opacity: 0;
animation: slideInMessage 0.3s ease-out forwards;
}
@keyframes slideInMessage {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-bubble {
max-width: 100%;
word-wrap: break-word;
position: relative;
transition: all 0.2s ease;
}
.message-bubble:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
.user-message .message-bubble {
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%) !important;
color: white;
border: none !important;
}
.bot-message .message-bubble {
background-color: #ffffff;
border: 1px solid #e9ecef;
color: #495057;
}
.avatar {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
}
.avatar:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.bot-message .avatar {
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%) !important;
}
.user-message .avatar {
background: linear-gradient(135deg, var(--secondary-color) 0%, #495057 100%) !important;
}
.typing-dots {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 0;
}
.typing-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.4;
}
40% {
transform: scale(1.2);
opacity: 1;
}
}
.file-icon {
font-size: 1.2rem;
}
.progress-bar-animated {
animation: progress-bar-stripes 1s linear infinite;
}
@keyframes progress-bar-stripes {
0% {
background-position: 1rem 0;
}
100% {
background-position: 0 0;
}
}
.btn {
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.navbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.footer {
margin-top: auto;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.hero-section {
min-height: 50vh;
text-align: center;
}
.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 {
font-size: 1rem !important;
}
.message-bubble {
padding: 0.75rem !important;
font-size: 0.9rem;
}
.card-body {
padding: 1.5rem;
}
#chat-container {
height: 400px !important;
padding: 1rem !important;
}
.chat-message {
margin-bottom: 1rem;
}
}
/* Loading spinner */
.spinner-border {
width: 3rem;
height: 3rem;
}
/* Custom scrollbar for chat */
#chat-container {
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
}
#chat-container::-webkit-scrollbar {
width: 8px;
}
#chat-container::-webkit-scrollbar-track {
background: #f8f9fa;
border-radius: 4px;
}
#chat-container::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #c1c1c1 0%, #a8a8a8 100%);
border-radius: 4px;
}
#chat-container::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #a8a8a8 0%, #888888 100%);
}
/* File upload styling */
.upload-area {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 2rem;
text-align: center;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
.upload-area:hover {
border-color: var(--primary-color);
background-color: #e7f3ff;
}
.upload-area.dragover {
border-color: var(--primary-color);
background-color: #e7f3ff;
transform: scale(1.02);
}
/* Success states */
.upload-success {
border-color: var(--success-color);
background-color: #d4edda;
}
/* Error states */
.upload-error {
border-color: var(--danger-color);
background-color: #f8d7da;
}
/* Animation for page transitions */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Quiz Section Styles */
.bg-gradient-warning {
background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%);
}
.quiz-preview {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.quiz-features .fas {
font-size: 1.1rem;
}
.quiz-question {
border-left: 4px solid var(--primary-color);
padding-left: 1rem;
}
.quiz-options .form-check-label {
cursor: pointer;
padding: 0.5rem 0;
}
.quiz-options .form-check-input:checked + .form-check-label {
font-weight: bold;
color: var(--success-color);
}
/* Chat input styling */
#chat-input {
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
#chat-input:focus {
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2) !important;
border-color: var(--primary-color) !important;
transform: translateY(-1px);
}
#send-btn {
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
#send-btn:hover {
background: linear-gradient(135deg, #0056b3 0%, #004085 100%);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
#send-btn:active {
transform: translateY(0);
}
/* Chat container enhancements */
#chat-container {
background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%);
border-radius: 0;
}
.chat .card-header {
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%) !important;
border-radius: 0.375rem 0.375rem 0 0 !important;
}
/* Enhance chat card styling */
.chat .card {
border: none;
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.1) !important;
border-radius: 0.75rem;
overflow: hidden;
}

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

518
public/js/main.js Normal file
View File

@@ -0,0 +1,518 @@
// Main JavaScript file for EduCat
document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize file upload functionality
initializeFileUpload();
// Initialize drag and drop
initializeDragAndDrop();
// Add fade-in animation to cards
animateCards();
});
function initializeFileUpload() {
const fileInput = document.getElementById('noteFile');
const uploadBtn = document.getElementById('upload-btn');
const fileInfo = document.getElementById('file-info');
const fileName = document.getElementById('file-name');
const fileSize = document.getElementById('file-size');
const uploadProgress = document.getElementById('upload-progress');
const uploadResult = document.getElementById('upload-result');
const uploadArea = document.getElementById('upload-area');
if (!fileInput) return;
// Remove any existing event listeners to prevent duplicates
fileInput.removeEventListener('change', handleFileChange);
fileInput.addEventListener('change', handleFileChange);
if (uploadBtn) {
uploadBtn.removeEventListener('click', handleUploadClick);
uploadBtn.addEventListener('click', handleUploadClick);
}
function handleFileChange(e) {
const file = e.target.files[0];
if (file) {
showFileInfo(file);
uploadBtn.disabled = false;
}
}
function handleUploadClick(e) {
e.preventDefault();
uploadFile();
}
function showFileInfo(file) {
if (fileName && fileSize && fileInfo) {
fileName.textContent = file.name;
fileSize.textContent = `(${formatFileSize(file.size)})`;
fileInfo.classList.remove('d-none');
}
}
function uploadFile() {
const file = fileInput.files[0];
if (!file) return;
const formData = new FormData();
formData.append('noteFile', file);
uploadBtn.disabled = true;
uploadProgress.classList.remove('d-none');
uploadResult.innerHTML = '';
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
uploadProgress.classList.add('d-none');
if (result.success) {
if (result.processing) {
// Show processing status
uploadResult.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-spinner fa-spin me-2"></i>
<strong>Upload Successful!</strong> ${result.message}
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center">
<span>Processing document chunks for AI analysis...</span>
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
</div>
<div class="mt-2">
<small class="text-muted">This may take a few moments depending on document size.</small>
</div>
</div>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="checkUploadStatus('${result.fileInfo.id}')">
<i class="fas fa-sync me-1"></i>Check Status
</button>
<a href="/dashboard" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-tachometer-alt me-1"></i>View Dashboard
</a>
</div>
</div>
`;
// Start polling for status updates
startStatusPolling(result.fileInfo.id);
} else {
// Immediate success (shouldn't happen with new flow)
uploadResult.innerHTML = `
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>
<strong>Success!</strong> ${result.message}
<div class="mt-2">
<a href="/revise/${result.fileInfo.id}" class="btn btn-sm btn-primary">
<i class="fas fa-brain me-1"></i>Revise with AI
</a>
<a href="/dashboard" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-tachometer-alt me-1"></i>View Dashboard
</a>
</div>
</div>
`;
}
// Reset form
fileInput.value = '';
fileInfo.classList.add('d-none');
uploadBtn.disabled = true;
// Add success animation
uploadArea.classList.add('upload-success');
setTimeout(() => {
uploadArea.classList.remove('upload-success');
}, 2000);
} else {
uploadResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Error!</strong> ${result.error}
${result.details ? `<br><small class="text-muted">${result.details}</small>` : ''}
</div>
`;
uploadArea.classList.add('upload-error');
setTimeout(() => {
uploadArea.classList.remove('upload-error');
}, 2000);
}
})
.catch(error => {
uploadProgress.classList.add('d-none');
uploadResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Error!</strong> ${error.message}
</div>
`;
uploadArea.classList.add('upload-error');
setTimeout(() => {
uploadArea.classList.remove('upload-error');
}, 2000);
})
.finally(() => {
uploadBtn.disabled = false;
});
}
}
function initializeDragAndDrop() {
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('noteFile');
if (!uploadArea || !fileInput) return;
uploadArea.addEventListener('click', function(e) {
// Only trigger file input if clicking on the area itself, not the button
if (e.target === uploadArea || e.target.tagName === 'I' || e.target.tagName === 'H5' || e.target.tagName === 'P') {
fileInput.click();
}
});
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
fileInput.dispatchEvent(new Event('change'));
}
});
}
function animateCards() {
const cards = document.querySelectorAll('.card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Utility functions
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
// Add to page
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
document.body.appendChild(toastContainer);
}
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// Remove after hiding
toast.addEventListener('hidden.bs.toast', function() {
toast.remove();
});
}
function showLoading(element) {
element.innerHTML = `
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading...
</div>
`;
}
function hideLoading(element, originalContent) {
element.innerHTML = originalContent;
}
// Error handling
window.addEventListener('error', function(e) {
console.error('Global error:', e.error);
showToast('An unexpected error occurred. Please try again.', 'danger');
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', function(e) {
console.error('Unhandled promise rejection:', e.reason);
showToast('An unexpected error occurred. Please try again.', 'danger');
});
// Add smooth scrolling for anchor links (but not for dropdown triggers)
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
const href = anchor.getAttribute('href');
// Skip empty anchors and dropdown triggers
if (href === '#' || anchor.hasAttribute('data-bs-toggle')) {
return;
}
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(href);
if (target) {
target.scrollIntoView({
behavior: 'smooth'
});
}
});
});
// Add loading states to buttons (but not for login/register/quiz forms)
document.querySelectorAll('.btn').forEach(button => {
if (button.type === 'submit' || button.hasAttribute('data-loading')) {
// Skip login, register, and quiz form buttons
const form = button.closest('form');
if (form && (form.action.includes('/login') || form.action.includes('/register') || form.id === 'quizForm')) {
return;
}
button.addEventListener('click', function() {
if (!this.disabled) {
const originalText = this.innerHTML;
this.innerHTML = `
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Loading...
`;
this.disabled = true;
// Re-enable after 5 seconds (fallback)
setTimeout(() => {
this.innerHTML = originalText;
this.disabled = false;
}, 5000);
}
});
}
});
// Status polling for document processing
let statusPollingInterval = null;
function startStatusPolling(fileId) {
// Clear any existing polling
if (statusPollingInterval) {
clearInterval(statusPollingInterval);
}
// Poll every 3 seconds
statusPollingInterval = setInterval(() => {
checkUploadStatus(fileId, true);
}, 3000);
}
function stopStatusPolling() {
if (statusPollingInterval) {
clearInterval(statusPollingInterval);
statusPollingInterval = null;
}
}
async function checkUploadStatus(fileId, isPolling = false) {
try {
const response = await fetch(`/api/files/${fileId}/status`);
const result = await response.json();
if (result.success) {
const file = result.file;
const uploadResult = document.getElementById('upload-result');
if (file.status === 'processed') {
// Processing completed successfully
if (uploadResult) {
uploadResult.innerHTML = `
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>
<strong>Processing Complete!</strong> Your document has been processed and is ready for AI analysis.
<div class="mt-3">
<div class="row">
<div class="col-md-6">
<small class="text-muted">
<i class="fas fa-puzzle-piece me-1"></i>
${file.processingResult ? `${file.processingResult.successfulChunks}/${file.processingResult.totalChunks} chunks processed` : 'Processed successfully'}
</small>
</div>
<div class="col-md-6">
<small class="text-muted">
<i class="fas fa-clock me-1"></i>
${file.processingResult && file.processingResult.processedAt ? new Date(file.processingResult.processedAt).toLocaleTimeString() : 'Just now'}
</small>
</div>
</div>
</div>
<div class="mt-3">
<a href="/revise/${file.id}" class="btn btn-sm btn-success">
<i class="fas fa-brain me-1"></i>Revise with AI
</a>
<a href="/dashboard" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-tachometer-alt me-1"></i>View Dashboard
</a>
</div>
</div>
`;
}
stopStatusPolling();
} else if (file.status === 'failed') {
// Processing failed
if (uploadResult) {
uploadResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Processing Failed!</strong> There was an error processing your document.
${file.processingError ? `<br><small class="text-muted">Error: ${file.processingError}</small>` : ''}
<div class="mt-3">
<button class="btn btn-sm btn-warning" onclick="retryDocumentProcessing('${file.id}')">
<i class="fas fa-redo me-1"></i>Retry Processing
</button>
<a href="/dashboard" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-tachometer-alt me-1"></i>View Dashboard
</a>
</div>
</div>
`;
}
stopStatusPolling();
} else if (file.status === 'processing' && !isPolling) {
// Still processing, manual check
let progressInfo = '';
if (file.processingResult) {
progressInfo = `<br><small class="text-muted">Progress: ${file.processingResult.successfulChunks || 0}/${file.processingResult.totalChunks || '?'} chunks</small>`;
}
if (uploadResult) {
uploadResult.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-spinner fa-spin me-2"></i>
<strong>Still Processing...</strong> Your document is being processed for AI analysis.
${progressInfo}
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="checkUploadStatus('${file.id}')">
<i class="fas fa-sync me-1"></i>Refresh Status
</button>
<a href="/dashboard" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-tachometer-alt me-1"></i>View Dashboard
</a>
</div>
</div>
`;
}
// Start polling if not already polling
if (!statusPollingInterval) {
startStatusPolling(fileId);
}
}
} else {
console.error('Error checking status:', result.error);
if (!isPolling) {
alert('Error checking status: ' + result.error);
}
}
} catch (error) {
console.error('Error checking upload status:', error);
if (!isPolling) {
alert('Error checking upload status: ' + error.message);
}
}
}
async function retryDocumentProcessing(fileId) {
try {
const response = await fetch(`/api/files/${fileId}/retry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
// Update UI to show retry started
const uploadResult = document.getElementById('upload-result');
if (uploadResult) {
uploadResult.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-spinner fa-spin me-2"></i>
<strong>Retry Started!</strong> Processing your document again...
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="checkUploadStatus('${fileId}')">
<i class="fas fa-sync me-1"></i>Check Status
</button>
<a href="/dashboard" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-tachometer-alt me-1"></i>View Dashboard
</a>
</div>
</div>
`;
}
// Start polling again
startStatusPolling(fileId);
} else {
alert('Error retrying processing: ' + result.error);
}
} catch (error) {
console.error('Error retrying processing:', error);
alert('Error retrying processing: ' + error.message);
}
}
// Clean up polling when page unloads
window.addEventListener('beforeunload', function() {
stopStatusPolling();
});

1628
server.js Normal file

File diff suppressed because it is too large Load Diff

196
views/chat.ejs Normal file
View File

@@ -0,0 +1,196 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-lg border-0 chat">
<div class="card-header bg-primary text-white">
<h3 class="mb-0"><i class="fas fa-comments me-2"></i>Chat with EduCat AI</h3>
</div>
<div class="card-body p-0">
<div id="chat-container" class="p-4" style="height: 500px; overflow-y: auto;">
<div class="chat-message bot-message mb-3">
<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;">
<i class="fas fa-cat" style="font-size: 1.2rem;"></i>
</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>
<small class="text-muted d-block mt-1">Just now</small>
</div>
</div>
</div>
</div>
<div class="border-top bg-light p-3">
<div class="input-group">
<input type="text" id="chat-input" class="form-control border-0 bg-white shadow-sm" placeholder="Type your message here..." style="border-radius: 25px 0 0 25px; padding: 12px 20px;">
<button type="button" id="send-btn" class="btn btn-primary px-4 border-0" style="border-radius: 0 25px 25px 0;">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-question-circle fa-2x text-primary mb-3"></i>
<h6>Ask Questions</h6>
<p class="text-muted small">Get answers about your study materials</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-graduation-cap fa-2x text-success mb-3"></i>
<h6>Study Help</h6>
<p class="text-muted small">Get study tips and techniques</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let chatHistory = [];
document.addEventListener('DOMContentLoaded', function() {
const chatContainer = document.getElementById('chat-container');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
sendBtn.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
function sendMessage() {
const message = chatInput.value.trim();
if (!message) return;
// Add user message to chat
addMessageToChat('user', message);
chatInput.value = '';
// Add typing indicator
addTypingIndicator();
// Send message to AI
sendToAI(message);
}
function addMessageToChat(sender, message) {
const chatContainer = document.getElementById('chat-container');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${sender}-message mb-3`;
const time = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
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>
<small class="text-muted d-block text-end mt-1">${time}</small>
</div>
<div class="avatar bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center flex-shrink-0" style="width: 45px; height: 45px; min-width: 45px;">
<i class="fas fa-user" style="font-size: 1.1rem;"></i>
</div>
</div>
`;
} else {
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;">
<i class="fas fa-cat" style="font-size: 1.2rem;"></i>
</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>
<small class="text-muted d-block mt-1">${time}</small>
</div>
</div>
`;
}
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function addTypingIndicator() {
const chatContainer = document.getElementById('chat-container');
const typingDiv = document.createElement('div');
typingDiv.id = 'typing-indicator';
typingDiv.className = 'chat-message bot-message mb-3';
typingDiv.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;">
<i class="fas fa-cat" style="font-size: 1.2rem;"></i>
</div>
<div class="message-content flex-grow-1">
<div class="message-bubble bg-light p-3 rounded-3 shadow-sm border">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
`;
chatContainer.appendChild(typingDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function removeTypingIndicator() {
const typingIndicator = document.getElementById('typing-indicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
async function sendToAI(message) {
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
history: chatHistory
})
});
const result = await response.json();
removeTypingIndicator();
if (result.success) {
addMessageToChat('bot', result.response);
chatHistory.push({human: message, ai: result.response});
} else {
addMessageToChat('bot', 'Sorry, I encountered an error. Please try again.');
}
} catch (error) {
removeTypingIndicator();
addMessageToChat('bot', 'Sorry, I\'m having trouble connecting right now. Please try again.');
}
}
});
</script>
<%- include('partials/footer') %>

671
views/dashboard.ejs Normal file
View File

@@ -0,0 +1,671 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-tachometer-alt me-2"></i>Your Dashboard</h2>
<a href="/upload" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Upload New Notes
</a>
</div>
<% if (files.length === 0) { %>
<div class="text-center py-5">
<i class="fas fa-folder-open fa-4x text-muted mb-4"></i>
<h4>No files uploaded yet</h4>
<p class="text-muted">Upload your first set of notes to get started with AI-powered revision.</p>
<a href="/upload" class="btn btn-primary btn-lg">
<i class="fas fa-upload me-2"></i>Upload Notes
</a>
</div>
<% } else { %>
<div class="row">
<% files.forEach(function(file, index) { %>
<div class="col-md-6 col-lg-4 mb-4">
<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;">
<i class="fas fa-file-alt"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-1"><%= file.originalName %></h6>
<small class="text-muted"><%= Math.round(file.size / 1024) %> KB</small>
</div>
<div class="text-end">
<% if (file.status === 'processing') { %>
<span class="badge bg-warning">
<i class="fas fa-spinner fa-spin me-1"></i>Processing
</span>
<% } else if (file.status === 'processed') { %>
<span class="badge bg-success">
<i class="fas fa-check me-1"></i>Processed
</span>
<% } else if (file.status === 'failed') { %>
<span class="badge bg-danger">
<i class="fas fa-times me-1"></i>Failed
</span>
<% } else { %>
<span class="badge bg-secondary">
<i class="fas fa-clock me-1"></i>Uploaded
</span>
<% } %>
</div>
</div>
<div class="mb-3">
<small class="text-muted">
<i class="fas fa-calendar me-1"></i>
Uploaded: <%= new Date(file.uploadDate).toLocaleDateString() %>
</small>
<% if (file.processingResult) { %>
<br><small class="text-muted">
<i class="fas fa-puzzle-piece me-1"></i>
Chunks: <%= file.processingResult.successfulChunks %>/<%= file.processingResult.totalChunks %>
</small>
<% } %>
</div>
<div class="d-grid gap-2">
<% if (file.status === 'processed') { %>
<a href="/revise/<%= file.id %>" class="btn btn-primary btn-sm">
<i class="fas fa-brain me-2"></i>Revise with AI
</a>
<% } else if (file.status === 'processing') { %>
<button class="btn btn-secondary btn-sm" disabled>
<i class="fas fa-spinner fa-spin me-2"></i>Processing...
</button>
<% } else if (file.status === 'failed') { %>
<button class="btn btn-warning btn-sm" onclick="retryProcessing('<%= file.id %>')">
<i class="fas fa-redo me-2"></i>Retry Processing
</button>
<% } else { %>
<button class="btn btn-info btn-sm" onclick="checkProcessingStatus('<%= file.id %>')">
<i class="fas fa-sync me-2"></i>Check Status
</button>
<% } %>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="previewFile('<%= file.id %>')">
<i class="fas fa-eye"></i> Preview
</button>
<button type="button" class="btn btn-outline-info btn-sm" onclick="viewProcessingDetails('<%= file.id %>')">
<i class="fas fa-info-circle"></i> Details
</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteFile('<%= file.id %>')">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
<% } %>
</div>
</div>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">File Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="preview-content">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading preview...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Processing Details Modal -->
<div class="modal fade" id="processingDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Processing Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="processing-details-content">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading processing details...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function previewFile(fileId) {
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
const previewContent = document.getElementById('preview-content');
previewContent.innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading preview...</p>
</div>
`;
modal.show();
// Fetch file content for preview
fetch(`/api/files/${fileId}/preview`)
.then(response => response.json())
.then(result => {
if (result.success) {
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)) {
// Text-based files - show with syntax highlighting
content = `
<div class="mb-3">
<h6><i class="fas fa-file-alt 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="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;"><code>${escapeHtml(file.content)}</code></pre>
</div>
`;
} else if (['pdf', 'doc', 'docx'].includes(fileExtension)) {
// Document files - show basic info and content preview
content = `
<div class="mb-3">
<h6><i class="fas fa-file-pdf 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-info">
<i class="fas fa-info-circle me-2"></i>
Document preview: First few lines of extracted text
</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.substring(0, 1000))}${file.content.length > 1000 ? '...' : ''}</pre>
</div>
`;
} else {
// Other files - show basic info
content = `
<div class="mb-3">
<h6><i class="fas fa-file 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>
Preview not available for this file type. File content is available for AI processing.
</div>
`;
}
previewContent.innerHTML = content;
} else {
previewContent.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
Error loading preview: ${result.error || 'Unknown error'}
</div>
`;
}
})
.catch(error => {
previewContent.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
Error loading preview: ${error.message}
</div>
`;
});
}
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function deleteFile(fileId) {
if (confirm('Are you sure you want to delete this file? This action cannot be undone.')) {
// Show loading state
const deleteBtn = document.querySelector(`button[onclick="deleteFile('${fileId}')"]`);
const originalText = deleteBtn.innerHTML;
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
deleteBtn.disabled = true;
fetch(`/api/files/${fileId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(result => {
if (result.success) {
// Show success message
alert('File deleted successfully!');
location.reload();
} else {
alert('Error deleting file: ' + (result.error || 'Unknown error'));
deleteBtn.innerHTML = originalText;
deleteBtn.disabled = false;
}
})
.catch(error => {
console.error('Delete error:', error);
alert('Error deleting file: ' + error.message);
deleteBtn.innerHTML = originalText;
deleteBtn.disabled = false;
});
}
}
// Check processing status
async function checkProcessingStatus(fileId) {
try {
const response = await fetch(`/api/files/${fileId}/status`);
const result = await response.json();
if (result.success) {
// Show status update
const statusInfo = result.file;
let message = `Status: ${statusInfo.status}`;
if (statusInfo.processingResult) {
message += `\nChunks processed: ${statusInfo.processingResult.successfulChunks}/${statusInfo.processingResult.totalChunks}`;
if (statusInfo.processingResult.processedAt) {
message += `\nProcessed at: ${new Date(statusInfo.processingResult.processedAt).toLocaleString()}`;
}
}
if (statusInfo.processingError) {
message += `\nError: ${statusInfo.processingError}`;
}
alert(message);
// Reload page if status changed
if (statusInfo.status !== 'processing') {
location.reload();
}
} else {
alert('Error checking status: ' + result.error);
}
} catch (error) {
console.error('Error checking processing status:', error);
alert('Error checking processing status: ' + error.message);
}
}
// Retry processing
async function retryProcessing(fileId) {
if (confirm('Are you sure you want to retry processing this file?')) {
try {
const response = await fetch(`/api/files/${fileId}/retry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
alert('Processing retry initiated. Please check back in a few moments.');
location.reload();
} else {
alert('Error retrying processing: ' + result.error);
}
} catch (error) {
console.error('Error retrying processing:', error);
alert('Error retrying processing: ' + error.message);
}
}
}
// View processing details
async function viewProcessingDetails(fileId) {
try {
const modal = new bootstrap.Modal(document.getElementById('processingDetailsModal'));
const detailsContent = document.getElementById('processing-details-content');
detailsContent.innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading processing details...</p>
</div>
`;
modal.show();
const response = await fetch(`/api/files/${fileId}/status`);
const result = await response.json();
if (result.success) {
const file = result.file;
let html = `
<div class="mb-3">
<h6><i class="fas fa-file-alt me-2"></i>${escapeHtml(file.originalName)}</h6>
<small class="text-muted">Uploaded: ${new Date(file.uploadDate).toLocaleString()}</small>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Status:</strong>
<span class="ms-2 badge ${file.status === 'processed' ? 'bg-success' : file.status === 'processing' ? 'bg-warning' : file.status === 'failed' ? 'bg-danger' : 'bg-secondary'}">
${file.status}
</span>
</div>
</div>
`;
if (file.processingResult) {
html += `
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0">Processing Results</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<strong>Total Chunks:</strong><br>
<span class="badge bg-info">${file.processingResult.totalChunks}</span>
</div>
<div class="col-md-4">
<strong>Successful:</strong><br>
<span class="badge bg-success">${file.processingResult.successfulChunks}</span>
</div>
<div class="col-md-4">
<strong>Failed:</strong><br>
<span class="badge bg-danger">${file.processingResult.failedChunks}</span>
</div>
</div>
${file.processingResult.processedAt ? `
<div class="mt-3">
<strong>Processed At:</strong><br>
<small class="text-muted">${new Date(file.processingResult.processedAt).toLocaleString()}</small>
</div>
` : ''}
${file.processingResult.documentHash ? `
<div class="mt-3">
<strong>Document Hash:</strong><br>
<small class="text-muted font-monospace">${file.processingResult.documentHash}</small>
</div>
` : ''}
${file.processingResult.contentLength ? `
<div class="mt-3">
<strong>Content Length:</strong><br>
<small class="text-muted">${file.processingResult.contentLength.toLocaleString()} characters</small>
</div>
` : ''}
</div>
</div>
`;
}
if (file.processingError) {
html += `
<div class="alert alert-danger">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Processing Error</h6>
<p class="mb-0">${escapeHtml(file.processingError)}</p>
</div>
`;
}
if (file.processingErrors && file.processingErrors.length > 0) {
html += `
<div class="card">
<div class="card-header">
<h6 class="mb-0">Chunk Errors (${file.processingErrors.length})</h6>
</div>
<div class="card-body" style="max-height: 300px; overflow-y: auto;">
`;
file.processingErrors.forEach((error, index) => {
html += `
<div class="border-bottom pb-2 mb-2">
<strong>Chunk ${error.chunkIndex + 1}:</strong><br>
<small class="text-danger">${escapeHtml(error.error)}</small><br>
<small class="text-muted">${escapeHtml(error.chunk)}</small>
</div>
`;
});
html += `
</div>
</div>
`;
}
detailsContent.innerHTML = html;
} else {
detailsContent.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
Error loading processing details: ${result.error}
</div>
`;
}
} catch (error) {
console.error('Error loading processing details:', error);
document.getElementById('processing-details-content').innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
Error loading processing details: ${error.message}
</div>
`;
}
}
// Progress monitoring for processing files
let progressMonitoring = {};
function startProgressMonitoring() {
// Find all processing files and start monitoring them
const processingCards = document.querySelectorAll('.card');
processingCards.forEach(card => {
const badge = card.querySelector('.badge');
if (badge && badge.textContent && badge.textContent.includes('Processing')) {
// Extract file ID from the card's buttons
const buttons = card.querySelectorAll('button[onclick*="Details"]');
if (buttons.length > 0) {
const onclick = buttons[0].getAttribute('onclick');
const fileIdMatch = onclick.match(/viewProcessingDetails\('([^']+)'\)/);
if (fileIdMatch) {
const fileId = fileIdMatch[1];
startFileProgressMonitoring(fileId);
}
}
}
});
}
function startFileProgressMonitoring(fileId) {
if (progressMonitoring[fileId]) {
clearInterval(progressMonitoring[fileId]);
}
console.log(`Starting progress monitoring for file: ${fileId}`);
progressMonitoring[fileId] = setInterval(async () => {
try {
const response = await fetch(`/api/files/${fileId}/progress`);
const result = await response.json();
if (result.success) {
updateProgressDisplay(fileId, result.progress);
// Stop monitoring if processing is complete
if (result.progress.status !== 'processing') {
clearInterval(progressMonitoring[fileId]);
delete progressMonitoring[fileId];
// Refresh page to show final status
setTimeout(() => {
location.reload();
}, 2000);
}
}
} catch (error) {
console.error('Error checking progress:', error);
}
}, 2000); // Check every 2 seconds
}
function updateProgressDisplay(fileId, progress) {
console.log('Updating progress for', fileId, progress);
// Find the card for this file
const cards = document.querySelectorAll('.card');
let targetCard = null;
cards.forEach(card => {
const buttons = card.querySelectorAll('button[onclick*="Details"]');
if (buttons.length > 0) {
const onclick = buttons[0].getAttribute('onclick');
if (onclick.includes(fileId)) {
targetCard = card;
}
}
});
if (!targetCard) {
console.warn('Target card not found for file:', fileId);
return;
}
// Update the badge with progress information
const badge = targetCard.querySelector('.badge');
if (badge && progress.processingProgress) {
const { currentChunk, totalChunks, percentage } = progress.processingProgress;
badge.innerHTML = `
<i class="fas fa-spinner fa-spin me-1"></i>
Processing ${currentChunk}/${totalChunks} (${percentage}%)
`;
badge.className = 'badge bg-warning text-dark';
} else if (badge && progress.status === 'processed') {
badge.innerHTML = `<i class="fas fa-check-circle me-1"></i>Processed`;
badge.className = 'badge bg-success';
} else if (badge && progress.status === 'failed') {
badge.innerHTML = `<i class="fas fa-exclamation-triangle me-1"></i>Failed`;
badge.className = 'badge bg-danger';
}
// Update or add progress bar
const cardBody = targetCard.querySelector('.card-body');
let progressContainer = cardBody.querySelector('.progress-container');
if (progress.processingProgress) {
if (!progressContainer) {
progressContainer = document.createElement('div');
progressContainer.className = 'progress-container mt-2';
cardBody.appendChild(progressContainer);
}
const { currentChunk, totalChunks, percentage } = progress.processingProgress;
progressContainer.innerHTML = `
<div class="progress mb-2" style="height: 8px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: ${percentage}%"
aria-valuenow="${percentage}"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
<small class="text-muted">
<i class="fas fa-puzzle-piece me-1"></i>
Processing chunk ${currentChunk} of ${totalChunks}
</small>
`;
} else if (progressContainer && (progress.status === 'processed' || progress.status === 'failed')) {
// Remove progress bar when processing is complete
progressContainer.remove();
}
}
function stopAllProgressMonitoring() {
Object.keys(progressMonitoring).forEach(fileId => {
clearInterval(progressMonitoring[fileId]);
delete progressMonitoring[fileId];
});
}
// Clean up when page unloads
window.addEventListener('beforeunload', stopAllProgressMonitoring);
// Auto-refresh processing status every 10 seconds for files that are still processing
document.addEventListener('DOMContentLoaded', function() {
// Start progress monitoring for processing files
startProgressMonitoring();
// Check if there are processing files by looking for badges with "Processing" text
const badges = document.querySelectorAll('.badge');
let hasProcessingFiles = false;
badges.forEach(badge => {
if (badge && badge.textContent && badge.textContent.includes('Processing')) {
hasProcessingFiles = true;
}
});
if (hasProcessingFiles) {
console.log('Found processing files, setting up auto-refresh...');
setInterval(() => {
// Check again if there are still processing files
const currentBadges = document.querySelectorAll('.badge');
let stillProcessing = false;
currentBadges.forEach(badge => {
if (badge && badge.textContent && badge.textContent.includes('Processing')) {
stillProcessing = true;
}
});
if (stillProcessing) {
console.log('Still have processing files, refreshing page...');
location.reload();
}
}, 10000); // Check every 10 seconds
}
});
</script>
<%- include('partials/footer') %>

20
views/error.ejs Normal file
View File

@@ -0,0 +1,20 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow-lg border-0">
<div class="card-body text-center py-5">
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-4"></i>
<h2 class="mb-3">Oops! Something went wrong</h2>
<p class="text-muted mb-4"><%= error %></p>
<a href="/" class="btn btn-primary">
<i class="fas fa-home me-2"></i>Go Home
</a>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer') %>

224
views/index.ejs Normal file
View File

@@ -0,0 +1,224 @@
<%- include('partials/header') %>
<!-- Display flash messages -->
<% if (messages.error) { %>
<div class="alert alert-danger alert-dismissible fade show m-3" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<%= messages.error %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<% if (messages.success) { %>
<div class="alert alert-success alert-dismissible fade show m-3" role="alert">
<i class="fas fa-check-circle me-2"></i>
<%= messages.success %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<div class="hero-section bg-gradient-primary text-white py-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<% if (user) { %>
<h1 class="display-4 fw-bold mb-4">Welcome back, <%= user.name %>!</h1>
<p class="lead mb-4">Ready to enhance your study experience? Upload notes, generate AI quizzes, get personalized revision, and chat with our intelligent assistant.</p>
<div class="d-flex gap-3 flex-wrap">
<a href="/upload" class="btn btn-light btn-lg">
<i class="fas fa-upload me-2"></i>Upload Notes
</a>
<a href="/quiz" class="btn btn-outline-light btn-lg">
<i class="fas fa-question-circle me-2"></i>Take Quiz
</a>
</div>
<% } else { %>
<h1 class="display-4 fw-bold mb-4">Welcome to EduCat</h1>
<p class="lead mb-4">Transform your study experience with AI-powered note revision, personalized quizzes, and intelligent tutoring. Upload your notes and let our smart system create the perfect study environment for you.</p>
<div class="d-flex gap-3 flex-wrap">
<a href="/login" class="btn btn-light btn-lg">
<i class="fas fa-sign-in-alt me-2"></i>Get Started
</a>
<a href="/register" class="btn btn-outline-light btn-lg">
<i class="fas fa-user-plus me-2"></i>Sign Up Free
</a>
</div>
<% } %>
</div>
<div class="col-lg-6 text-center">
<img src="/images/logo.png" alt="EduCat" class="img-fluid" style="max-height: 300px;">
</div>
</div>
</div>
</div>
<div class="container py-5">
<div class="row">
<div class="col-md-3 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
<i class="fas fa-upload fa-2x"></i>
</div>
<h5 class="card-title">Upload Your Notes</h5>
<p class="card-text">Simply upload your study notes in various formats (PDF, DOC, TXT) and let our AI analyze them.</p>
<% if (user) { %>
<a href="/upload" class="btn btn-primary">Get Started</a>
<% } else { %>
<a href="/login" class="btn btn-primary">Get Started</a>
<% } %>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-success text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
<i class="fas fa-brain fa-2x"></i>
</div>
<h5 class="card-title">AI-Powered Revision</h5>
<p class="card-text">Our AI will summarize, improve, and generate study questions from your notes automatically.</p>
<% if (user) { %>
<a href="/dashboard" class="btn btn-success">View Dashboard</a>
<% } else { %>
<a href="/login" class="btn btn-success">View Dashboard</a>
<% } %>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-warning text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
<i class="fas fa-question-circle fa-2x"></i>
</div>
<h5 class="card-title">AI Quiz Generator</h5>
<p class="card-text">Generate personalized quizzes on any topic with multiple question types and difficulty levels.</p>
<% if (user) { %>
<a href="/quiz" class="btn btn-warning">Take Quiz</a>
<% } else { %>
<a href="/login" class="btn btn-warning">Take Quiz</a>
<% } %>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-info text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
<i class="fas fa-comments fa-2x"></i>
</div>
<h5 class="card-title">Interactive Chat</h5>
<p class="card-text">Ask questions about your notes and get instant, intelligent responses from our AI assistant.</p>
<% if (user) { %>
<a href="/chat" class="btn btn-info">Start Chatting</a>
<% } else { %>
<a href="/login" class="btn btn-info">Start Chatting</a>
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Quiz Section -->
<div class="bg-gradient-warning py-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h2 class="display-5 fw-bold text-white mb-4">Test Your Knowledge</h2>
<p class="lead text-white mb-4">Generate personalized quizzes on any topic with our AI-powered quiz generator. Choose from multiple question types and difficulty levels to challenge yourself.</p>
<div class="quiz-features mb-4">
<div class="d-flex align-items-center text-white mb-2">
<i class="fas fa-check-circle me-3"></i>
<span>Multiple choice questions</span>
</div>
<div class="d-flex align-items-center text-white mb-2">
<i class="fas fa-check-circle me-3"></i>
<span>True/False questions</span>
</div>
<div class="d-flex align-items-center text-white mb-2">
<i class="fas fa-check-circle me-3"></i>
<span>Instant feedback and scoring</span>
</div>
<div class="d-flex align-items-center text-white mb-2">
<i class="fas fa-check-circle me-3"></i>
<span>Customizable difficulty levels</span>
</div>
</div>
<% if (user) { %>
<a href="/quiz" class="btn btn-light btn-lg">
<i class="fas fa-question-circle me-2"></i>Start Quiz
</a>
<% } else { %>
<a href="/login" class="btn btn-light btn-lg">
<i class="fas fa-sign-in-alt me-2"></i>Login to Start
</a>
<% } %>
</div>
<div class="col-lg-6 text-center">
<div class="quiz-preview bg-white rounded-3 shadow-lg p-4 mx-auto" style="max-width: 400px;">
<h6 class="text-primary mb-3">Sample Quiz Question</h6>
<div class="quiz-question mb-3">
<p class="fw-bold mb-3">What is the capital of France?</p>
<div class="quiz-options">
<div class="form-check text-start mb-2">
<input class="form-check-input" type="radio" name="sample" disabled>
<label class="form-check-label">London</label>
</div>
<div class="form-check text-start mb-2">
<input class="form-check-input" type="radio" name="sample" checked disabled>
<label class="form-check-label">Paris ✓</label>
</div>
<div class="form-check text-start mb-2">
<input class="form-check-input" type="radio" name="sample" disabled>
<label class="form-check-label">Berlin</label>
</div>
<div class="form-check text-start">
<input class="form-check-input" type="radio" name="sample" disabled>
<label class="form-check-label">Madrid</label>
</div>
</div>
</div>
<div class="text-success small">
<i class="fas fa-check me-1"></i>Correct! Great job.
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-light py-5">
<div class="container">
<div class="row">
<div class="col-lg-10 mx-auto text-center">
<h2 class="mb-4">How EduCat Works</h2>
<div class="row">
<div class="col-md-3">
<div class="step-number bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 50px; height: 50px;">1</div>
<h6>Upload</h6>
<p class="text-muted">Upload your study notes in any supported format</p>
</div>
<div class="col-md-3">
<div class="step-number bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 50px; height: 50px;">2</div>
<h6>AI Analysis</h6>
<p class="text-muted">Our AI analyzes and processes your content</p>
</div>
<div class="col-md-3">
<div class="step-number bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 50px; height: 50px;">3</div>
<h6>Take Quizzes</h6>
<p class="text-muted">Generate personalized quizzes to test your knowledge</p>
</div>
<div class="col-md-3">
<div class="step-number bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 50px; height: 50px;">4</div>
<h6>Get Results</h6>
<p class="text-muted">Receive improved notes, summaries, and instant feedback</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer') %>

61
views/layout.ejs Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="/images/logo.png" alt="EduCat Logo" height="40" class="me-2">
<span class="fw-bold">EduCat</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-home"></i> Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/upload"><i class="fas fa-upload"></i> Upload Notes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/dashboard"><i class="fas fa-tachometer-alt"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/chat"><i class="fas fa-comments"></i> Chat</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid">
<%- body %>
</main>
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>EduCat</h5>
<p>AI-powered note revision platform for students</p>
</div>
<div class="col-md-6 text-end">
<p>&copy; 2025 EduCat. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>

151
views/login.ejs Normal file
View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<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">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="/images/logo.png" alt="EduCat Logo" height="40" class="me-2">
<span class="fw-bold">EduCat</span>
</a>
</div>
</nav>
<div class="container-fluid vh-100 d-flex align-items-center justify-content-center bg-light">
<div class="row w-100">
<div class="col-md-6 col-lg-4 mx-auto">
<div class="card shadow-lg border-0">
<div class="card-body p-5">
<div class="text-center mb-4">
<img src="/images/logo.png" alt="EduCat Logo" height="80" class="mb-3">
<h2 class="mb-2">Welcome Back!</h2>
<p class="text-muted">Sign in to your EduCat account</p>
</div>
<!-- Display flash messages -->
<% if (messages.error) { %>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<%= messages.error %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<% if (messages.success) { %>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>
<%= messages.success %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<form action="/login" method="POST" id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username or Email</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword()">
<i class="fas fa-eye" id="toggleIcon"></i>
</button>
</div>
</div>
<div class="d-grid gap-2 mb-3">
<button type="submit" class="btn btn-primary btn-lg" id="loginBtn">
<i class="fas fa-sign-in-alt me-2"></i>Sign In
</button>
</div>
</form>
<div class="text-center">
<p class="text-muted">Don't have an account?
<a href="/register" class="text-primary text-decoration-none">Sign up here</a>
</p>
</div>
</div>
</div>
<div class="card mt-3 border-0 shadow-sm">
<div class="card-body text-center">
<h6 class="text-muted mb-3">Demo Accounts</h6>
<div class="row">
<div class="col-6">
<small class="text-muted">
<strong>Admin:</strong><br>
Username: admin<br>
Password: password
</small>
</div>
<div class="col-6">
<small class="text-muted">
<strong>Student:</strong><br>
Username: student<br>
Password: password
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleIcon = document.getElementById('toggleIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
}
}
// Form validation and submission handling
document.getElementById('loginForm').addEventListener('submit', function(e) {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
if (!username || !password) {
e.preventDefault();
alert('Please fill in all fields');
return;
}
// Show loading state
loginBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Signing In...';
loginBtn.disabled = true;
// Form will submit normally, loading state will be cleared when page changes
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
</body>
</html>

18
views/partials/footer.ejs Normal file
View File

@@ -0,0 +1,18 @@
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>EduCat</h5>
<p>AI-powered note revision platform for students</p>
</div>
<div class="col-md-6 text-end">
<p>&copy; 2025 EduCat. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>

64
views/partials/header.ejs Normal file
View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="/images/logo.png" alt="EduCat Logo" height="40" class="me-2">
<span class="fw-bold">EduCat</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-home"></i> Home</a>
</li>
<% if (user) { %>
<li class="nav-item">
<a class="nav-link" href="/upload"><i class="fas fa-upload"></i> Upload Notes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/dashboard"><i class="fas fa-tachometer-alt"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/quiz"><i class="fas fa-question-circle"></i> Quiz</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/quiz-history"><i class="fas fa-history"></i> History</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/chat"><i class="fas fa-comments"></i> Chat</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> <%= user.name %>
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/dashboard"><i class="fas fa-tachometer-alt me-2"></i>Dashboard</a></li>
<li><a class="dropdown-item" href="/quiz-history"><i class="fas fa-history me-2"></i>Quiz History</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout"><i class="fas fa-sign-out-alt me-2"></i>Logout</a></li>
</ul>
</li>
<% } else { %>
<li class="nav-item">
<a class="nav-link" href="/login"><i class="fas fa-sign-in-alt"></i> Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/register"><i class="fas fa-user-plus"></i> Register</a>
</li>
<% } %>
</ul>
</div>
</div>
</nav>

549
views/quiz-history.ejs Normal file
View File

@@ -0,0 +1,549 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row">
<div class="col-12">
<h2 class="mb-4"><i class="fas fa-history me-2"></i>Quiz History</h2>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4" id="stats-cards">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<i class="fas fa-list-ol fa-2x mb-2"></i>
<h4 id="total-quizzes">-</h4>
<p class="mb-0">Total Quizzes</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<i class="fas fa-chart-line fa-2x mb-2"></i>
<h4 id="average-score">-%</h4>
<p class="mb-0">Average Score</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<i class="fas fa-trophy fa-2x mb-2"></i>
<h4 id="best-score">-%</h4>
<p class="mb-0">Best Score</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<i class="fas fa-brain fa-2x mb-2"></i>
<h4 id="favorite-topic">-</h4>
<p class="mb-0">Favorite Topic</p>
</div>
</div>
</div>
</div>
<!-- Progress Chart -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Progress Over Time</h5>
</div>
<div class="card-body">
<canvas id="progressChart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
<!-- Topic Statistics -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-tags me-2"></i>Performance by Topic</h5>
</div>
<div class="card-body">
<div id="topic-stats" class="row">
<!-- Topic stats will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</div>
<!-- Quiz History Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-table me-2"></i>All Quiz Results</h5>
<% if (quizResults.length > 0) { %>
<button class="btn btn-outline-danger btn-sm" onclick="clearHistory()">
<i class="fas fa-trash me-2"></i>Clear History
</button>
<% } %>
</div>
<div class="card-body">
<% if (quizResults.length === 0) { %>
<div class="text-center py-5">
<i class="fas fa-clipboard-list fa-4x text-muted mb-3"></i>
<h4 class="text-muted">No Quiz History Yet</h4>
<p class="text-muted mb-4">Take your first quiz to see your results here!</p>
<a href="/quiz" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Take Your First Quiz
</a>
</div>
<% } else { %>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th><i class="fas fa-calendar me-2"></i>Date</th>
<th><i class="fas fa-book me-2"></i>Topic</th>
<th><i class="fas fa-layer-group me-2"></i>Difficulty</th>
<th><i class="fas fa-question-circle me-2"></i>Type</th>
<th><i class="fas fa-chart-pie me-2"></i>Score</th>
<th><i class="fas fa-percentage me-2"></i>Percentage</th>
<th><i class="fas fa-medal me-2"></i>Grade</th>
<th><i class="fas fa-eye me-2"></i>Actions</th>
</tr>
</thead>
<tbody>
<% quizResults.forEach(function(quiz) { %>
<%
let grade = 'F';
let badgeClass = 'bg-danger';
if (quiz.percentage >= 90) { grade = 'A'; badgeClass = 'bg-success'; }
else if (quiz.percentage >= 80) { grade = 'B'; badgeClass = 'bg-info'; }
else if (quiz.percentage >= 70) { grade = 'C'; badgeClass = 'bg-warning'; }
else if (quiz.percentage >= 60) { grade = 'D'; badgeClass = 'bg-warning'; }
%>
<tr>
<td><%= new Date(quiz.date).toLocaleDateString() %></td>
<td>
<span class="badge bg-secondary"><%= quiz.topic %></span>
</td>
<td>
<span class="badge bg-primary"><%= quiz.difficulty || 'unknown' %></span>
</td>
<td><%= quiz.quizType || 'multiple-choice' %></td>
<td><%= quiz.score %>/<%= quiz.total %></td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar bg-success" role="progressbar"
style="width: <%= quiz.percentage %>%"
aria-valuenow="<%= quiz.percentage %>"
aria-valuemin="0" aria-valuemax="100">
<%= quiz.percentage %>%
</div>
</div>
</td>
<td>
<span class="badge <%= badgeClass %>"><%= grade %></span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary"
onclick="viewQuizDetails('<%= quiz.id %>')">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Quiz Details Modal -->
<div class="modal fade" id="quizDetailsModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Quiz Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="quizDetailsContent" style="max-height: 70vh; overflow-y: auto;">
<!-- Quiz details will be loaded here -->
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadQuizStatistics();
});
async function loadQuizStatistics() {
try {
const response = await fetch('/api/quiz-stats');
const data = await response.json();
if (data.success) {
updateStatisticsCards(data.stats);
createProgressChart(data.stats.progressChart);
displayTopicStats(data.stats.topicStats);
}
} catch (error) {
console.error('Error loading quiz statistics:', error);
}
}
function updateStatisticsCards(stats) {
document.getElementById('total-quizzes').textContent = stats.totalQuizzes;
document.getElementById('average-score').textContent = stats.averageScore + '%';
document.getElementById('best-score').textContent = stats.bestScore + '%';
// Find favorite topic (most quizzes taken)
let favoriteTopicName = '-';
let maxCount = 0;
Object.keys(stats.topicStats).forEach(topic => {
if (stats.topicStats[topic].count > maxCount) {
maxCount = stats.topicStats[topic].count;
favoriteTopicName = topic;
}
});
document.getElementById('favorite-topic').textContent = favoriteTopicName;
}
let progressChartInstance = null;
function createProgressChart(progressData) {
const ctx = document.getElementById('progressChart').getContext('2d');
// Destroy existing chart if it exists
if (progressChartInstance) {
progressChartInstance.destroy();
}
if (progressData.length === 0) {
ctx.font = '16px Arial';
ctx.fillStyle = '#6c757d';
ctx.textAlign = 'center';
ctx.fillText('No quiz data available yet', ctx.canvas.width / 2, ctx.canvas.height / 2);
return;
}
progressChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: progressData.map((_, index) => `Quiz ${index + 1}`),
datasets: [{
label: 'Score (%)',
data: progressData.map(quiz => quiz.score),
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
plugins: {
title: {
display: true,
text: 'Your Quiz Performance Over Time'
}
}
}
});
}
function displayTopicStats(topicStats) {
const container = document.getElementById('topic-stats');
if (Object.keys(topicStats).length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted">No topic data available yet</div>';
return;
}
let html = '';
Object.keys(topicStats).forEach(topic => {
const stats = topicStats[topic];
html += `
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h6 class="card-title">${topic}</h6>
<p class="card-text">
<small class="text-muted">Quizzes taken: ${stats.count}</small><br>
<small class="text-muted">Average: ${stats.averageScore}%</small><br>
<small class="text-muted">Best: ${stats.bestScore}%</small>
</p>
<div class="progress" style="height: 10px;">
<div class="progress-bar" style="width: ${stats.averageScore}%"></div>
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
async function viewQuizDetails(quizId) {
try {
// Show loading spinner
document.getElementById('quizDetailsContent').innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading quiz details...</p>
</div>
`;
// Show modal first
new bootstrap.Modal(document.getElementById('quizDetailsModal')).show();
// Fetch quiz details
const response = await fetch(`/api/quiz-details/${quizId}`);
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to load quiz details');
}
const quiz = data.quiz;
const correctAnswers = quiz.results.filter(r => r.isCorrect).length;
const totalQuestions = quiz.results.length;
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
let html = `
<div class="mb-4">
<h6 class="text-muted">Quiz Overview</h6>
<div class="row">
<div class="col-md-3">
<strong>Topic:</strong><br>
<span class="badge bg-primary">${escapeHtml(quiz.topic)}</span>
</div>
<div class="col-md-3">
<strong>Score:</strong><br>
<span class="badge bg-${quiz.percentage >= 70 ? 'success' : quiz.percentage >= 50 ? 'warning' : 'danger'}">
${quiz.percentage}%
</span>
</div>
<div class="col-md-3">
<strong>Correct:</strong><br>
${correctAnswers} / ${totalQuestions}
</div>
<div class="col-md-3">
<strong>Date:</strong><br>
${new Date(quiz.date).toLocaleDateString()}
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<strong>Difficulty:</strong><br>
<span class="badge bg-info">${escapeHtml(quiz.difficulty)}</span>
</div>
<div class="col-md-6">
<strong>Quiz Type:</strong><br>
<span class="badge bg-secondary">${escapeHtml(quiz.quizType)}</span>
</div>
</div>
</div>
<h6 class="text-muted mb-3">Question-by-Question Results</h6>
<div class="quiz-questions">
`;
quiz.results.forEach((result, index) => {
html += `
<div class="card mb-3 ${result.isCorrect ? 'border-success' : 'border-danger'}">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Question ${index + 1}</h6>
<span class="badge bg-${result.isCorrect ? 'success' : 'danger'}">
${result.isCorrect ? 'Correct' : 'Incorrect'}
</span>
</div>
<div class="card-body">
<p class="mb-3"><strong>Question:</strong> ${escapeHtml(result.question)}</p>
${result.options ? `
<div class="mb-3">
<strong>Options:</strong>
<ul class="list-unstyled mt-2">
${result.options.map(option => `
<li class="mb-1">
<span class="badge bg-light text-dark me-2">${escapeHtml(option)}</span>
${option === result.userAnswer ? '<i class="fas fa-arrow-left text-primary" title="Your answer"></i>' : ''}
${option === result.correctAnswer ? '<i class="fas fa-check text-success" title="Correct answer"></i>' : ''}
</li>
`).join('')}
</ul>
</div>
` : ''}
<p class="mb-2"><strong>Your Answer:</strong></p>
<div class="alert alert-${result.isCorrect ? 'success' : 'danger'} py-2">
${escapeHtml(result.userAnswer || 'No answer provided')}
</div>
${!result.isCorrect && result.correctAnswer ? `
<p class="mb-2"><strong>Correct Answer:</strong></p>
<div class="alert alert-success py-2">
${escapeHtml(result.correctAnswer)}
</div>
` : ''}
${result.explanation ? `
<p class="mb-2"><strong>Explanation:</strong></p>
<div class="alert alert-info py-2">
${escapeHtml(result.explanation)}
</div>
` : ''}
</div>
</div>
`;
});
html += `</div>`;
document.getElementById('quizDetailsContent').innerHTML = html;
} catch (error) {
console.error('Error loading quiz details:', error);
document.getElementById('quizDetailsContent').innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Failed to load quiz details. Please try again.
</div>
`;
}
}
async function clearHistory() {
if (confirm('Are you sure you want to clear all quiz history? This action cannot be undone.')) {
try {
// Show loading state
const clearBtn = document.querySelector('button[onclick="clearHistory()"]');
const originalText = clearBtn.innerHTML;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Clearing...';
clearBtn.disabled = true;
const response = await fetch('/api/quiz-history', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
// Show success message
alert('Quiz history cleared successfully!');
// Clear the statistics cards
document.getElementById('total-quizzes').textContent = '0';
document.getElementById('average-score').textContent = '0%';
document.getElementById('best-score').textContent = '0%';
document.getElementById('favorite-topic').textContent = 'None';
// Clear the topic statistics section
const topicStats = document.getElementById('topic-stats');
if (topicStats) {
topicStats.innerHTML = `
<div class="col-12 text-center py-4">
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Topic Data Available</h5>
<p class="text-muted">Take quizzes to see performance by topic.</p>
</div>
`;
}
// Clear the quiz results table - find the card body that contains the table
const tableCards = document.querySelectorAll('.card');
for (let card of tableCards) {
const cardHeader = card.querySelector('.card-header h5');
if (cardHeader && cardHeader.textContent.includes('All Quiz Results')) {
const cardBody = card.querySelector('.card-body');
if (cardBody) {
cardBody.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-clipboard-list fa-4x text-muted mb-3"></i>
<h4 class="text-muted">No Quiz History Yet</h4>
<p class="text-muted mb-4">Take your first quiz to see your results here!</p>
<a href="/quiz" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Take Your First Quiz
</a>
</div>
`;
}
// Also update the card header to remove the clear button
const cardHeader = card.querySelector('.card-header');
if (cardHeader) {
cardHeader.innerHTML = '<h5 class="mb-0"><i class="fas fa-table me-2"></i>All Quiz Results</h5>';
}
break;
}
}
// Clear the progress chart
if (progressChartInstance) {
progressChartInstance.destroy();
progressChartInstance = null;
}
// Clear the progress chart canvas and show no data message
const progressChartCanvas = document.getElementById('progressChart');
if (progressChartCanvas) {
const ctx = progressChartCanvas.getContext('2d');
ctx.clearRect(0, 0, progressChartCanvas.width, progressChartCanvas.height);
ctx.font = '16px Arial';
ctx.fillStyle = '#6c757d';
ctx.textAlign = 'center';
ctx.fillText('No quiz data available yet', progressChartCanvas.width / 2, progressChartCanvas.height / 2);
}
} else {
alert('Error clearing history: ' + (result.error || 'Unknown error'));
clearBtn.innerHTML = originalText;
clearBtn.disabled = false;
}
} catch (error) {
console.error('Error clearing quiz history:', error);
alert('Error clearing history: ' + error.message);
// Reset button on error
const clearBtn = document.querySelector('button[onclick="clearHistory()"]');
if (clearBtn) {
clearBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Clear History';
clearBtn.disabled = false;
}
}
}
}
</script>
<%- include('partials/footer') %>

642
views/quiz.ejs Normal file
View File

@@ -0,0 +1,642 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row">
<div class="col-12">
<h2 class="mb-4"><i class="fas fa-question-circle me-2"></i>AI Quiz Generator</h2>
</div>
</div>
<!-- Quiz Generator Form -->
<div id="quiz-generator" class="row">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-cogs me-2"></i>Generate New Quiz</h4>
</div>
<div class="card-body">
<form id="quizForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="topic" class="form-label">Topic/Subject</label>
<input type="text" class="form-control" id="topic" name="topic" placeholder="e.g., JavaScript, Biology, History" required>
</div>
<div class="col-md-6 mb-3">
<label for="difficulty" class="form-label">Difficulty Level</label>
<select class="form-select" id="difficulty" name="difficulty" required>
<option value="">Select Difficulty</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="questionCount" class="form-label">Number of Questions</label>
<select class="form-select" id="questionCount" name="questionCount" required>
<option value="">Select Count</option>
<option value="5">5 Questions</option>
<option value="10">10 Questions</option>
<option value="15">15 Questions</option>
<option value="20">20 Questions</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="quizType" class="form-label">Quiz Type</label>
<select class="form-select" id="quizType" name="quizType" required>
<option value="">Select Type</option>
<option value="multiple-choice">Multiple Choice</option>
<option value="true-false">True/False</option>
<option value="short-answer">Short Answer</option>
</select>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-magic me-2"></i>Generate Quiz
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5><i class="fas fa-info-circle me-2"></i>How It Works</h5>
<ul class="list-unstyled">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Choose your topic and difficulty</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Select question count and type</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>AI generates personalized quiz</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Take the quiz and get instant feedback</li>
</ul>
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-body">
<h5><i class="fas fa-lightbulb me-2"></i>Tips</h5>
<ul class="small">
<li>Be specific with your topic for better questions</li>
<li>Start with beginner if you're new to the subject</li>
<li>Multiple choice is great for concept testing</li>
<li>True/False is perfect for fact checking</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div id="quiz-loading" class="text-center py-5 d-none">
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h4>Generating Your Quiz...</h4>
<p class="text-muted">Our AI is creating personalized questions for you</p>
</div>
<!-- Quiz Container -->
<div id="quiz-container" class="d-none">
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0" id="quiz-title">Quiz</h4>
<div>
<span class="badge bg-light text-dark" id="quiz-progress">Question 1 of 10</span>
</div>
</div>
<div class="card-body">
<div id="quiz-questions"></div>
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-outline-secondary" id="prev-btn" disabled>
<i class="fas fa-chevron-left me-2"></i>Previous
</button>
<button type="button" class="btn btn-primary" id="next-btn">
Next<i class="fas fa-chevron-right ms-2"></i>
</button>
<button type="button" class="btn btn-success d-none" id="submit-btn">
<i class="fas fa-check me-2"></i>Submit Quiz
</button>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5><i class="fas fa-list me-2"></i>Quiz Overview</h5>
<div id="quiz-overview"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Results Container -->
<div id="results-container" class="d-none">
<div class="row">
<div class="col-lg-8">
<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>
</div>
<div class="card-body">
<div id="results-summary" class="text-center mb-4"></div>
<div id="results-details"></div>
<div class="text-center mt-4">
<button type="button" class="btn btn-primary me-2" id="new-quiz-btn">
<i class="fas fa-plus me-2"></i>Generate New Quiz
</button>
<a href="/quiz-history" class="btn btn-outline-info me-2">
<i class="fas fa-history me-2"></i>View History
</a>
<button type="button" class="btn btn-outline-secondary" id="review-btn">
<i class="fas fa-eye me-2"></i>Review Answers
</button>
</div>
</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>
<script>
let currentQuiz = null;
let currentQuestion = 0;
let userAnswers = [];
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
document.addEventListener('DOMContentLoaded', function() {
const quizForm = document.getElementById('quizForm');
const quizGenerator = document.getElementById('quiz-generator');
const quizLoading = document.getElementById('quiz-loading');
const quizContainer = document.getElementById('quiz-container');
const resultsContainer = document.getElementById('results-container');
console.log('Quiz page loaded');
if (!quizForm) {
console.error('Quiz form not found!');
alert('Error: Quiz form not found. Please refresh the page.');
return;
}
// Add event listener for form submission
quizForm.addEventListener('submit', async function(e) {
e.preventDefault();
console.log('Form submitted');
try {
await generateQuiz();
} catch (error) {
console.error('Error generating quiz:', error);
alert('Error generating quiz: ' + error.message);
if (quizGenerator) quizGenerator.classList.remove('d-none');
if (quizLoading) quizLoading.classList.add('d-none');
}
});
async function generateQuiz() {
console.log('Generating quiz...');
const formData = new FormData(quizForm);
const quizData = {
topic: formData.get('topic'),
difficulty: formData.get('difficulty'),
questionCount: parseInt(formData.get('questionCount')),
quizType: formData.get('quizType')
};
console.log('Quiz data:', quizData);
// Validate form data
if (!quizData.topic || !quizData.difficulty || !quizData.questionCount || !quizData.quizType) {
console.error('Validation failed:', quizData);
alert('Please fill in all fields');
return;
}
// Show loading
if (quizGenerator) quizGenerator.classList.add('d-none');
if (quizLoading) quizLoading.classList.remove('d-none');
try {
console.log('Sending request to API...');
const response = await fetch('/api/generate-quiz', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(quizData)
});
console.log('Response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('API Error:', response.status, errorText);
throw new Error(`API Error: ${response.status} - ${errorText}`);
}
const result = await response.json();
console.log('Quiz generated successfully');
if (result.success) {
currentQuiz = result.quiz;
currentQuestion = 0;
userAnswers = new Array(currentQuiz.length).fill(null);
displayQuiz(result);
} else {
console.error('Quiz generation failed:', result.error);
alert('Error generating quiz: ' + (result.error || 'Unknown error'));
if (quizGenerator) quizGenerator.classList.remove('d-none');
}
} catch (error) {
console.error('Quiz generation error:', error);
alert('Error: ' + error.message);
if (quizGenerator) quizGenerator.classList.remove('d-none');
} finally {
if (quizLoading) quizLoading.classList.add('d-none');
}
}
function displayQuiz(quizData) {
console.log('Displaying quiz:', quizData);
if (!quizContainer) {
console.error('Quiz container not found!');
return;
}
quizContainer.classList.remove('d-none');
const quizTitle = document.getElementById('quiz-title');
if (quizTitle) {
quizTitle.textContent = `${quizData.topic} Quiz`;
}
displayQuestion(currentQuestion);
displayOverview();
// Navigation buttons
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
const newQuizBtn = document.getElementById('new-quiz-btn');
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentQuestion > 0) {
currentQuestion--;
displayQuestion(currentQuestion);
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentQuestion < currentQuiz.length - 1) {
currentQuestion++;
displayQuestion(currentQuestion);
}
});
}
if (submitBtn) {
submitBtn.addEventListener('click', submitQuiz);
}
if (newQuizBtn) {
newQuizBtn.addEventListener('click', () => {
resetQuiz();
});
}
}
function displayQuestion(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;
}
let html = `
<div class="mb-4">
<h5 class="mb-3">Question ${index + 1}</h5>
<p class="lead">${escapeHtml(question.question)}</p>
</div>
`;
if (question.options) {
// Multiple choice
html += '<div class="mb-3">';
question.options.forEach((option, i) => {
const isSelected = userAnswers[index] === option.charAt(0);
html += `
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="question_${index}"
id="q${index}_${i}" value="${escapeHtml(option.charAt(0))}" ${isSelected ? 'checked' : ''}>
<label class="form-check-label" for="q${index}_${i}">
${escapeHtml(option)}
</label>
</div>
`;
});
html += '</div>';
} else if (question.correct === 'True' || question.correct === 'False') {
// True/False
html += `
<div class="mb-3">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="question_${index}"
id="q${index}_true" value="True" ${userAnswers[index] === 'True' ? 'checked' : ''}>
<label class="form-check-label" for="q${index}_true">True</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="question_${index}"
id="q${index}_false" value="False" ${userAnswers[index] === 'False' ? 'checked' : ''}>
<label class="form-check-label" for="q${index}_false">False</label>
</div>
</div>
`;
} else {
// Short answer
html += `
<div class="mb-3">
<textarea class="form-control" name="question_${index}"
placeholder="Enter your answer here...">${userAnswers[index] || ''}</textarea>
</div>
`;
}
questionsDiv.innerHTML = html;
// Update progress
const progressEl = document.getElementById('quiz-progress');
if (progressEl) {
progressEl.textContent = `Question ${index + 1} of ${currentQuiz.length}`;
}
// Update navigation buttons
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
// Check if all questions are answered
const allAnswered = userAnswers.every(answer => answer !== null && answer !== '');
if (prevBtn) prevBtn.disabled = index === 0;
// Show next button if not on last question
if (nextBtn) {
if (index === currentQuiz.length - 1) {
nextBtn.classList.add('d-none');
} else {
nextBtn.classList.remove('d-none');
}
}
// Show submit button if all questions answered or on last question
if (submitBtn) {
if (allAnswered) {
submitBtn.classList.remove('d-none');
submitBtn.innerHTML = '<i class="fas fa-trophy me-2"></i>Complete Quiz';
submitBtn.classList.remove('btn-success');
submitBtn.classList.add('btn-primary');
} else if (index === currentQuiz.length - 1) {
submitBtn.classList.remove('d-none');
submitBtn.innerHTML = '<i class="fas fa-check me-2"></i>Submit Quiz';
submitBtn.classList.remove('btn-primary');
submitBtn.classList.add('btn-success');
} else {
submitBtn.classList.add('d-none');
}
}
// Save answer when changed
const inputs = questionsDiv.querySelectorAll('input, textarea');
inputs.forEach(input => {
input.addEventListener('change', (e) => {
userAnswers[index] = e.target.value;
displayOverview();
updateNavigationButtons(); // Update buttons when answers change
});
});
}
function updateNavigationButtons() {
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
// Check if all questions are answered
const allAnswered = userAnswers.every(answer => answer !== null && answer !== '');
if (prevBtn) prevBtn.disabled = currentQuestion === 0;
// Show next button if not on last question
if (nextBtn) {
if (currentQuestion === currentQuiz.length - 1) {
nextBtn.classList.add('d-none');
} else {
nextBtn.classList.remove('d-none');
}
}
// Show submit button if all questions answered or on last question
if (submitBtn) {
if (allAnswered) {
submitBtn.classList.remove('d-none');
submitBtn.innerHTML = '<i class="fas fa-trophy me-2"></i>Complete Quiz';
submitBtn.classList.remove('btn-success');
submitBtn.classList.add('btn-primary');
} else if (currentQuestion === currentQuiz.length - 1) {
submitBtn.classList.remove('d-none');
submitBtn.innerHTML = '<i class="fas fa-check me-2"></i>Submit Quiz';
submitBtn.classList.remove('btn-primary');
submitBtn.classList.add('btn-success');
} else {
submitBtn.classList.add('d-none');
}
}
}
function displayOverview() {
const overviewDiv = document.getElementById('quiz-overview');
let html = '<div class="row">';
currentQuiz.forEach((_, i) => {
const isAnswered = userAnswers[i] !== null && userAnswers[i] !== '';
const isCurrent = i === currentQuestion;
html += `
<div class="col-4 mb-2">
<button class="btn btn-sm w-100 ${isCurrent ? 'btn-primary' : isAnswered ? 'btn-success' : 'btn-outline-secondary'}"
onclick="goToQuestion(${i})">
${i + 1}
</button>
</div>
`;
});
html += '</div>';
overviewDiv.innerHTML = html;
// Update navigation buttons when overview changes
updateNavigationButtons();
}
window.goToQuestion = function(index) {
currentQuestion = index;
displayQuestion(currentQuestion);
updateNavigationButtons();
};
async function submitQuiz() {
try {
const response = await fetch('/api/submit-quiz', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
answers: userAnswers,
quiz: currentQuiz,
topic: document.getElementById('topic').value,
difficulty: document.getElementById('difficulty').value,
quizType: document.getElementById('quizType').value
})
});
const result = await response.json();
if (result.success) {
displayResults(result);
} else {
alert('Error submitting quiz: ' + result.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
}
function displayResults(results) {
quizContainer.classList.add('d-none');
resultsContainer.classList.remove('d-none');
const summaryDiv = document.getElementById('results-summary');
const percentage = results.percentage;
let grade = 'F';
let badgeClass = 'bg-danger';
if (percentage >= 90) { grade = 'A'; badgeClass = 'bg-success'; }
else if (percentage >= 80) { grade = 'B'; badgeClass = 'bg-info'; }
else if (percentage >= 70) { grade = 'C'; badgeClass = 'bg-warning'; }
else if (percentage >= 60) { grade = 'D'; badgeClass = 'bg-warning'; }
summaryDiv.innerHTML = `
<div class="row">
<div class="col-md-3">
<div class="display-4 text-primary">${results.score}/${results.total}</div>
<p class="text-muted">Score</p>
</div>
<div class="col-md-3">
<div class="display-4 text-success">${percentage}%</div>
<p class="text-muted">Percentage</p>
</div>
<div class="col-md-3">
<div class="display-4">
<span class="badge ${badgeClass} fs-1">${grade}</span>
</div>
<p class="text-muted">Grade</p>
</div>
<div class="col-md-3">
<div class="display-4 text-info">${results.results.filter(r => r.isCorrect).length}</div>
<p class="text-muted">Correct</p>
</div>
</div>
`;
const detailsDiv = document.getElementById('results-details');
let detailsHtml = '<h5 class="mb-3">Detailed Results</h5>';
results.results.forEach((result, i) => {
detailsHtml += `
<div class="card mb-3 ${result.isCorrect ? 'border-success' : 'border-danger'}">
<div class="card-body">
<h6 class="card-title d-flex justify-content-between align-items-center">
Question ${i + 1}
<span class="badge ${result.isCorrect ? 'bg-success' : 'bg-danger'}">
${result.isCorrect ? 'Correct' : 'Incorrect'}
</span>
</h6>
<p class="card-text">${escapeHtml(result.question)}</p>
<div class="row">
<div class="col-md-6">
<small class="text-muted">Your Answer:</small>
<p class="mb-1 ${result.isCorrect ? 'text-success' : 'text-danger'}">${escapeHtml(result.userAnswer || 'Not answered')}</p>
</div>
<div class="col-md-6">
<small class="text-muted">Correct Answer:</small>
<p class="mb-1 text-success">${escapeHtml(result.correctAnswer)}</p>
</div>
</div>
${result.explanation ? `<div class="mt-2"><small class="text-muted">Explanation:</small><p class="small">${escapeHtml(result.explanation)}</p></div>` : ''}
</div>
</div>
`;
});
detailsDiv.innerHTML = detailsHtml;
}
function resetQuiz() {
currentQuiz = null;
currentQuestion = 0;
userAnswers = [];
quizContainer.classList.add('d-none');
resultsContainer.classList.add('d-none');
quizGenerator.classList.remove('d-none');
quizForm.reset();
}
});
</script>
<%- include('partials/footer') %>

194
views/register.ejs Normal file
View File

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<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">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="/images/logo.png" alt="EduCat Logo" height="40" class="me-2">
<span class="fw-bold">EduCat</span>
</a>
</div>
</nav>
<div class="container-fluid vh-100 d-flex align-items-center justify-content-center bg-light">
<div class="row w-100">
<div class="col-md-6 col-lg-5 mx-auto">
<div class="card shadow-lg border-0">
<div class="card-body p-5">
<div class="text-center mb-4">
<img src="/images/logo.png" alt="EduCat Logo" height="80" class="mb-3">
<h2 class="mb-2">Join EduCat!</h2>
<p class="text-muted">Create your account to get started</p>
</div>
<!-- Display flash messages -->
<% if (messages.error) { %>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<%= messages.error %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<% if (messages.success) { %>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>
<%= messages.success %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<form action="/register" method="POST" id="registerForm">
<div class="mb-3">
<label for="name" class="form-label">Full Name</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
<input type="text" class="form-control" id="name" name="name" required>
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-at"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-envelope"></i>
</span>
<input type="email" class="form-control" id="email" name="email" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required minlength="6">
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('password')">
<i class="fas fa-eye" id="toggleIcon1"></i>
</button>
</div>
<small class="text-muted">Password must be at least 6 characters long</small>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required minlength="6">
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('confirmPassword')">
<i class="fas fa-eye" id="toggleIcon2"></i>
</button>
</div>
</div>
<div class="d-grid gap-2 mb-3">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-user-plus me-2"></i>Create Account
</button>
</div>
</form>
<div class="text-center">
<p class="text-muted">Already have an account?
<a href="/login" class="text-primary text-decoration-none">Sign in here</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function togglePassword(fieldId) {
const passwordInput = document.getElementById(fieldId);
const toggleIcon = document.getElementById(fieldId === 'password' ? 'toggleIcon1' : 'toggleIcon2');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
}
}
// Form validation
document.getElementById('registerForm').addEventListener('submit', function(e) {
const name = document.getElementById('name').value.trim();
const username = document.getElementById('username').value.trim();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!name || !username || !email || !password || !confirmPassword) {
e.preventDefault();
alert('Please fill in all fields');
return;
}
if (password !== confirmPassword) {
e.preventDefault();
alert('Passwords do not match');
return;
}
if (password.length < 6) {
e.preventDefault();
alert('Password must be at least 6 characters long');
return;
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
e.preventDefault();
alert('Please enter a valid email address');
return;
}
});
// Real-time password confirmation
document.getElementById('confirmPassword').addEventListener('input', function() {
const password = document.getElementById('password').value;
const confirmPassword = this.value;
if (password !== confirmPassword) {
this.setCustomValidity('Passwords do not match');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
this.classList.add('is-valid');
}
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
</body>
</html>

149
views/revise.ejs Normal file
View File

@@ -0,0 +1,149 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row">
<div class="col-lg-8">
<div class="card shadow-lg border-0">
<div class="card-header bg-primary text-white">
<h3 class="mb-0"><i class="fas fa-edit me-2"></i>Revise: <%= file.originalName %></h3>
</div>
<div class="card-body">
<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>
</div>
</div>
<div class="col-md-6">
<h5>AI-Revised Notes</h5>
<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>
</div>
</div>
<div class="mt-4">
<div class="row">
<div class="col-md-6">
<label class="form-label">Revision Type</label>
<select id="revision-type" class="form-select">
<option value="improve">Improve & Enhance</option>
<option value="summarize">Summarize</option>
<option value="questions">Generate Study Questions</option>
</select>
</div>
<div class="col-md-6 d-flex align-items-end">
<button type="button" id="revise-btn" class="btn btn-primary btn-lg w-100">
<i class="fas fa-brain me-2"></i>Revise with AI
</button>
</div>
</div>
</div>
<div id="revision-progress" class="mt-4 d-none">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
</div>
<p class="text-center mt-2 mb-0">AI is processing your notes...</p>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5><i class="fas fa-info-circle me-2"></i>File Information</h5>
<ul class="list-unstyled">
<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>
</ul>
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-body">
<h5><i class="fas fa-lightbulb me-2"></i>Revision Tips</h5>
<ul class="small">
<li><strong>Improve & Enhance:</strong> Makes your notes more comprehensive and well-structured</li>
<li><strong>Summarize:</strong> Creates concise summaries of your key points</li>
<li><strong>Generate Questions:</strong> Creates study questions to test your understanding</li>
</ul>
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-body text-center">
<button type="button" id="save-btn" class="btn btn-success w-100 mb-2" disabled>
<i class="fas fa-save me-2"></i>Save Revised Notes
</button>
<button type="button" id="download-btn" class="btn btn-outline-primary w-100" disabled>
<i class="fas fa-download me-2"></i>Download
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const reviseBtn = document.getElementById('revise-btn');
const revisionProgress = document.getElementById('revision-progress');
const revisedContent = document.getElementById('revised-content');
const revisionType = document.getElementById('revision-type');
const saveBtn = document.getElementById('save-btn');
const downloadBtn = document.getElementById('download-btn');
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);
reviseBtn.disabled = true;
revisionProgress.classList.remove('d-none');
revisedContent.innerHTML = '<p class="text-muted text-center mt-5">Processing...</p>';
try {
console.log('Sending request to /api/revise');
const response = await fetch('/api/revise', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
revisionType: type
})
});
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>';
saveBtn.disabled = false;
downloadBtn.disabled = false;
} else {
revisedContent.innerHTML = '<div class="alert alert-danger">Error: ' + (result.error || 'Unknown error') + '</div>';
}
} catch (error) {
console.error('Revision error:', error);
revisedContent.innerHTML = '<div class="alert alert-danger">Error: ' + error.message + '</div>';
} finally {
reviseBtn.disabled = false;
revisionProgress.classList.add('d-none');
}
});
});
</script>
<%- include('partials/footer') %>

75
views/upload.ejs Normal file
View File

@@ -0,0 +1,75 @@
<%- include('partials/header') %>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-lg border-0">
<div class="card-header bg-primary text-white">
<h3 class="mb-0"><i class="fas fa-upload me-2"></i>Upload Your Notes</h3>
</div>
<div class="card-body p-5">
<p class="text-muted mb-4">Upload your study notes and let our AI help you create better, more comprehensive study materials.</p>
<div id="upload-area" class="border-2 border-dashed border-primary rounded p-5 text-center mb-4 upload-dropzone">
<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">
<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 id="file-info" class="d-none mb-4">
<div class="alert alert-info">
<i class="fas fa-file me-2"></i>
<span id="file-name"></span>
<span id="file-size" class="text-muted ms-2"></span>
</div>
</div>
<div class="d-grid gap-2">
<button type="button" id="upload-btn" class="btn btn-success btn-lg" disabled>
<i class="fas fa-upload me-2"></i>Upload & Process
</button>
</div>
<div id="upload-progress" class="mt-4 d-none">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
<p class="text-center mt-2 mb-0">Processing your notes...</p>
</div>
<div id="upload-result" class="mt-4"></div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-4">
<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="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="text-center">
<i class="fas fa-file-alt fa-2x text-success mb-2"></i>
<h6>Text Files</h6>
<p class="text-muted small">Plain text documents</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer') %>