EduCat with Flowise integration - complete implementation
This commit is contained in:
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