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.
264 lines โข 11.8 kB
JavaScript
export class FeedbackManager {
constructor(settingsService) {
this.activeFeedback = new Set();
this.settingsService = settingsService;
}
showImmediateFeedback(element, isCorrect, questionId) {
const settings = this.settingsService.getSettings();
if (!settings.quiz.showImmediateFeedback) {
return;
}
console.log(`๐ก Showing immediate feedback: ${isCorrect ? 'Correct' : 'Incorrect'}`);
this.clearFeedback(element);
const feedbackClasses = isCorrect
? ['bg-green-100', 'border-green-500', 'dark:bg-green-900/20', 'animate-pulse']
: ['bg-red-100', 'border-red-500', 'dark:bg-red-900/20', 'animate-pulse'];
element.classList.add(...feedbackClasses);
this.addFeedbackIcon(element, isCorrect);
if (settings.quiz.enableCoachingMode) {
this.showCoachingFeedback(element, isCorrect, questionId);
}
if (settings.quiz.showExplanationsAfterAnswer) {
setTimeout(() => {
this.showExplanation(questionId, isCorrect);
}, 800);
}
this.activeFeedback.add(questionId);
setTimeout(() => {
element.classList.remove('animate-pulse');
}, 2000);
this.announceToScreenReader(isCorrect ? 'Correct answer selected' : 'Incorrect answer selected');
}
showExplanation(questionId, wasCorrect) {
const settings = this.settingsService.getSettings();
if (!settings.quiz.showExplanationsAfterAnswer) {
return;
}
console.log(`๐ Showing explanation for question: ${questionId}`);
const questionContainer = document.querySelector(`[data-question-id="${questionId}"]`);
if (!questionContainer) {
console.warn('โ ๏ธ Question container not found for explanation');
return;
}
const existingExplanation = questionContainer.querySelector('.explanation-container');
if (existingExplanation) {
return;
}
const explanationText = this.getExplanationText(questionId);
if (!explanationText) {
return;
}
const explanationContainer = document.createElement('div');
explanationContainer.className = `
explanation-container mt-4 p-4 rounded-lg border-l-4
${wasCorrect
? 'bg-green-50 border-green-400 dark:bg-green-900/10 dark:border-green-500'
: 'bg-blue-50 border-blue-400 dark:bg-blue-900/10 dark:border-blue-500'}
animate-fadeIn
`;
explanationContainer.innerHTML = `
<div class="flex items-start">
<div class="flex-shrink-0">
${wasCorrect
? '<svg class="w-5 h-5 text-green-600 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>'
: '<svg class="w-5 h-5 text-blue-600 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>'}
</div>
<div class="ml-3">
<h4 class="text-sm font-medium ${wasCorrect ? 'text-green-800 dark:text-green-200' : 'text-blue-800 dark:text-blue-200'}">
${wasCorrect ? 'Great job!' : 'Learn more:'}
</h4>
<div class="mt-1 text-sm ${wasCorrect ? 'text-green-700 dark:text-green-300' : 'text-blue-700 dark:text-blue-300'}">
${explanationText}
</div>
</div>
</div>
`;
questionContainer.appendChild(explanationContainer);
setTimeout(() => {
explanationContainer.classList.add('opacity-100');
}, 50);
}
showCoachingFeedback(element, isCorrect, questionId) {
const settings = this.settingsService.getSettings();
if (!settings.quiz.enableCoachingMode) {
return;
}
console.log(`๐โโ๏ธ Showing coaching feedback for: ${questionId}`);
const intensity = settings.quiz.coachingIntensity || 'detailed';
const coachingMessages = this.getCoachingMessages(isCorrect, intensity);
const message = coachingMessages[Math.floor(Math.random() * coachingMessages.length)];
this.showCoachingTooltip(element, message, isCorrect);
}
showCelebration(element, intensity = 'normal') {
console.log(`๐ Showing ${intensity} celebration`);
switch (intensity) {
case 'subtle':
this.showSubtleCelebration(element);
break;
case 'enthusiastic':
this.showEnthusiasticCelebration(element);
break;
default:
this.showNormalCelebration(element);
}
}
clearFeedback(element) {
const feedbackClasses = [
'bg-green-100',
'border-green-500',
'dark:bg-green-900/20',
'bg-red-100',
'border-red-500',
'dark:bg-red-900/20',
'animate-pulse',
];
element.classList.remove(...feedbackClasses);
const feedbackIcon = element.querySelector('.feedback-icon');
if (feedbackIcon) {
feedbackIcon.remove();
}
}
clearAllFeedback() {
this.activeFeedback.forEach(questionId => {
const questionContainer = document.querySelector(`[data-question-id="${questionId}"]`);
if (questionContainer) {
const answerElements = questionContainer.querySelectorAll('.quiz-option-card, label');
answerElements.forEach(element => this.clearFeedback(element));
const explanations = questionContainer.querySelectorAll('.explanation-container');
explanations.forEach(explanation => explanation.remove());
}
});
this.activeFeedback.clear();
console.log('๐งน All feedback cleared');
}
addFeedbackIcon(element, isCorrect) {
const existingIcon = element.querySelector('.feedback-icon');
if (existingIcon) {
existingIcon.remove();
}
const icon = document.createElement('div');
icon.className =
'feedback-icon absolute top-2 right-2 w-6 h-6 rounded-full flex items-center justify-center';
if (isCorrect) {
icon.classList.add('bg-green-500', 'text-white');
icon.innerHTML =
'<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>';
}
else {
icon.classList.add('bg-red-500', 'text-white');
icon.innerHTML =
'<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>';
}
if (getComputedStyle(element).position === 'static') {
element.classList.add('relative');
}
element.appendChild(icon);
}
showCoachingTooltip(element, message, isCorrect) {
const tooltip = document.createElement('div');
tooltip.className = `
coaching-tooltip absolute z-50 px-3 py-2 text-sm rounded-lg shadow-lg max-w-xs
${isCorrect ? 'bg-green-600 text-white' : 'bg-blue-600 text-white'}
animate-fadeIn
`;
tooltip.textContent = message;
const rect = element.getBoundingClientRect();
tooltip.style.top = `${rect.bottom + 8}px`;
tooltip.style.left = `${rect.left}px`;
document.body.appendChild(tooltip);
setTimeout(() => {
tooltip.remove();
}, 3000);
}
getExplanationText(questionId) {
return "This explanation would be loaded from the question's explanation field.";
}
getCoachingMessages(isCorrect, intensity) {
if (isCorrect) {
switch (intensity) {
case 'comprehensive':
return ['Excellent work!', 'Outstanding!', 'Perfect!', 'You nailed it!'];
case 'basic':
return ['Correct', 'Good', 'Right'];
default:
return ['Great job!', 'Well done!', 'Correct!', 'Nice work!'];
}
}
else {
switch (intensity) {
case 'comprehensive':
return [
'Not quite right, but keep trying!',
'Good effort! Try again.',
'Close! Give it another shot.',
];
case 'basic':
return ['Try again', 'Not correct'];
default:
return ['Not quite right', 'Try again!', 'Keep going!'];
}
}
}
showSubtleCelebration(element) {
element.classList.add('animate-bounce');
setTimeout(() => {
element.classList.remove('animate-bounce');
}, 1000);
}
showNormalCelebration(element) {
element.classList.add('animate-bounce');
this.createSuccessParticles(element);
setTimeout(() => {
element.classList.remove('animate-bounce');
}, 1500);
}
showEnthusiasticCelebration(element) {
element.classList.add('animate-bounce');
for (let i = 0; i < 3; i++) {
setTimeout(() => {
this.createSuccessParticles(element);
}, i * 200);
}
setTimeout(() => {
element.classList.remove('animate-bounce');
}, 2000);
}
createSuccessParticles(element) {
const rect = element.getBoundingClientRect();
const particles = ['๐', 'โจ', '๐', '๐ซ'];
for (let i = 0; i < 5; i++) {
const particle = document.createElement('div');
particle.textContent = particles[Math.floor(Math.random() * particles.length)];
particle.className = 'fixed pointer-events-none z-50 text-2xl animate-bounce';
particle.style.left = `${rect.left + Math.random() * rect.width}px`;
particle.style.top = `${rect.top + Math.random() * rect.height}px`;
document.body.appendChild(particle);
setTimeout(() => {
particle.style.transform = `translateY(-50px) scale(0)`;
particle.style.opacity = '0';
particle.style.transition = 'all 1s ease-out';
setTimeout(() => {
particle.remove();
}, 1000);
}, 100);
}
}
announceToScreenReader(message) {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
cleanup() {
this.clearAllFeedback();
const tooltips = document.querySelectorAll('.coaching-tooltip');
tooltips.forEach(tooltip => tooltip.remove());
console.log('๐งน FeedbackManager cleanup completed');
}
}
//# sourceMappingURL=FeedbackManager.js.map