EduCat with Flowise integration - complete implementation
This commit is contained in:
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') %>
|
||||
Reference in New Issue
Block a user