EduCat with Flowise integration - complete implementation

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

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

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