mcp-quiz-server
Version:
🧠AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.
228 lines (226 loc) • 10.4 kB
JavaScript
import { AppStore } from '../store/AppStore';
import { DOMUtils, QuizUtils } from '../utils/index';
import { Component } from './Component';
export class ResultsModal extends Component {
constructor() {
super('#result-modal');
this.unsubscribe = null;
this.currentResult = null;
this.store = AppStore.getInstance();
}
onMount() {
this.unsubscribe = this.store.subscribe(state => this.onStateChange(state));
}
onUnmount() {
this.unsubscribe?.();
}
bindEvents() {
this.element.addEventListener('click', e => {
if (e.target === this.element) {
this.store.hideResults();
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
this.store.hideResults();
}
});
document.querySelector('#close-results-btn')?.addEventListener('click', () => {
this.store.hideResults();
});
}
onStateChange(state) {
if (state.ui.showResults && state.currentQuiz) {
this.show();
this.render();
}
else {
this.hide();
}
}
render() {
const state = this.store.getState();
if (!state.currentQuiz)
return;
const result = state.lastResult || this.createResultFromState(state);
this.currentResult = result;
const resultContainer = this.element.querySelector('#result-container');
if (!resultContainer)
return;
const percentage = Math.round((this.currentResult.score / this.currentResult.totalQuestions) * 100);
const grade = QuizUtils.getGrade(percentage);
const feedbackData = this.currentResult.feedback || this.currentResult.results || [];
const resultsHtml = feedbackData
?.map((questionResult, index) => {
const isCorrect = questionResult.isCorrect;
const bgColor = isCorrect
? 'bg-green-50 dark:bg-green-900/20'
: 'bg-red-50 dark:bg-red-900/20';
const borderColor = isCorrect
? 'border-green-200 dark:border-green-800'
: 'border-red-200 dark:border-red-800';
const iconColor = isCorrect
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400';
return `
<div class="p-3 md:p-4 rounded-lg border ${borderColor} ${bgColor}">
<div class="flex items-start gap-3 mb-2">
<i data-lucide="${isCorrect ? 'check-circle' : 'x-circle'}"
class="w-4 h-4 md:w-5 md:h-5 ${iconColor} mt-1 flex-shrink-0"></i>
<div class="flex-1 min-w-0">
<p class="text-sm md:text-base font-medium text-surface-900 dark:text-surface-100 leading-snug">
${index + 1}. ${DOMUtils.escapeHtml(questionResult.question)}
</p>
</div>
</div>
<div class="ml-7 md:ml-8 text-xs md:text-sm space-y-1.5">
<p class="text-surface-700 dark:text-surface-300">
<span class="font-medium">Your answer:</span>
<span class="${isCorrect ? 'text-green-700 dark:text-green-300' : 'text-red-700 dark:text-red-300'}">${DOMUtils.escapeHtml(questionResult.userAnswer)}</span>
</p>
${!isCorrect
? `
<p class="text-surface-700 dark:text-surface-300">
<span class="font-medium">Correct answer:</span>
<span class="text-green-700 dark:text-green-300">${DOMUtils.escapeHtml(questionResult.correctAnswer)}</span>
</p>
`
: ''}
${questionResult.explanation
? `
<div class="mt-2 md:mt-3 p-2 md:p-3 bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-400 dark:border-blue-500 rounded-r">
<div class="flex items-start gap-2">
<i data-lucide="lightbulb" class="w-3 h-3 md:w-4 md:h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0"></i>
<div class="flex-1">
<p class="text-xs md:text-sm font-medium text-blue-800 dark:text-blue-200 mb-1"> Explanation</p>
<p class="text-xs md:text-sm text-blue-700 dark:text-blue-300 leading-relaxed">${DOMUtils.escapeHtml(questionResult.explanation)}</p>
</div>
</div>
</div>
`
: ''}
</div>
</div>
`;
})
.join('') || '';
resultContainer.innerHTML = `
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 bg-${grade.color}-100 dark:bg-${grade.color}-900/30 rounded-full mb-4">
<i data-lucide="${grade.icon}" class="w-8 h-8 text-${grade.color}-600 dark:text-${grade.color}-400"></i>
</div>
<h3 class="text-xl md:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-2">${grade.text}</h3>
<p class="text-surface-600 dark:text-surface-400 text-sm md:text-base">
You scored ${this.currentResult.score} out of ${this.currentResult.totalQuestions} questions
</p>
<div class="text-2xl md:text-3xl font-bold text-${grade.color}-600 dark:text-${grade.color}-400 mt-2">${percentage}%</div>
</div>
<div class="space-y-3 md:space-y-4">
<h4 class="font-medium text-surface-900 dark:text-surface-100 text-sm md:text-base">Question Breakdown:</h4>
${resultsHtml}
</div>
`;
const actionsContainer = this.element.querySelector('#result-actions');
if (actionsContainer) {
actionsContainer.innerHTML = `
<div class="flex flex-col sm:flex-row gap-3">
<button id="retake-quiz-btn" class="flex-1 bg-primary-600 hover:bg-primary-700 text-white py-2.5 px-4 rounded-lg font-medium transition-colors text-sm md:text-base">
<i data-lucide="rotate-ccw" class="w-4 h-4 mr-2 inline"></i>
Retake Quiz
</button>
<button id="close-modal-btn" class="flex-1 bg-surface-200 hover:bg-surface-300 dark:bg-surface-600 dark:hover:bg-surface-500 text-surface-800 dark:text-surface-200 py-2.5 px-4 rounded-lg font-medium transition-colors text-sm md:text-base">
<i data-lucide="check" class="w-4 h-4 mr-2 inline"></i>
Done
</button>
</div>
`;
}
this.reinitializeIcons();
this.bindModalButtons();
}
createResultFromState(state) {
if (!state.currentQuiz)
return null;
const results = state.currentQuiz.questions.map(question => {
const userAnswer = state.userAnswers[question.id] || '';
const isCorrect = userAnswer === question.correctAnswer;
return {
question: question.question,
userAnswer: userAnswer,
correctAnswer: question.correctAnswer,
isCorrect: isCorrect,
explanation: question.explanation,
};
});
const score = results.filter(r => r.isCorrect).length;
return {
score: score,
totalQuestions: results.length,
results: results,
};
}
bindModalButtons() {
const retakeBtn = this.element.querySelector('#retake-quiz-btn');
const closeBtn = this.element.querySelector('#close-modal-btn');
retakeBtn?.addEventListener('click', () => {
this.store.hideResults();
const state = this.store.getState();
if (state.currentQuiz) {
state.currentQuiz.questions.forEach(q => {
this.store.updateAnswer(q.id, '');
});
}
});
closeBtn?.addEventListener('click', () => {
this.store.hideResults();
});
}
show() {
this.element.classList.remove('hidden');
this.element.classList.add('flex');
requestAnimationFrame(() => {
const modalContent = this.element.querySelector('.modal-content');
if (modalContent) {
modalContent.classList.remove('translate-y-full');
modalContent.style.opacity = '1';
modalContent.style.transform = 'scale(1)';
modalContent.classList.add('scale-100');
}
});
}
hide() {
const modalContent = this.element.querySelector('.modal-content');
if (modalContent) {
if (window.innerWidth < 768) {
modalContent.classList.add('translate-y-full');
}
else {
modalContent.style.transform = 'scale(0.95)';
modalContent.style.opacity = '0';
}
setTimeout(() => {
this.element.classList.add('hidden');
this.element.classList.remove('flex');
modalContent.classList.remove('translate-y-full');
modalContent.style.transform = '';
modalContent.style.opacity = '';
}, 300);
}
else {
this.element.classList.add('hidden');
this.element.classList.remove('flex');
}
}
showResult(result) {
this.currentResult = result;
this.show();
this.render();
}
reinitializeIcons() {
if (window.lucide?.createIcons) {
window.lucide.createIcons();
}
}
}
export default ResultsModal;
//# sourceMappingURL=ResultsModal.js.map