EduCat with Flowise integration - complete implementation
This commit is contained in:
85
.gitignore
vendored
Normal file
85
.gitignore
vendored
Normal 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
210
README.md
Normal 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
2293
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal 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
399
public/css/style.css
Normal 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
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
518
public/js/main.js
Normal 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();
|
||||
});
|
||||
196
views/chat.ejs
Normal file
196
views/chat.ejs
Normal 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
671
views/dashboard.ejs
Normal 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
20
views/error.ejs
Normal 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
224
views/index.ejs
Normal 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
61
views/layout.ejs
Normal 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>© 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
151
views/login.ejs
Normal 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
18
views/partials/footer.ejs
Normal 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>© 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
64
views/partials/header.ejs
Normal 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
549
views/quiz-history.ejs
Normal 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
642
views/quiz.ejs
Normal 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
194
views/register.ejs
Normal 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
149
views/revise.ejs
Normal 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
75
views/upload.ejs
Normal 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') %>
|
||||
Reference in New Issue
Block a user