
Quiz Trainer
Why ?
During my time at Lab4Tech, I was training to obtain different certifications like the
C++ CLA & CPP and the Unity Professional Programmer.
The only problem was that the question pool was so little, it made it impossible to truly train on a vast amout of questions.
How ?
To resolve that problem, I decided to create a simple but usefull web page what would intake a Json file (Json Doc. Here) and produce an interactive Quiz.
To do so, I used an AI chatbot and fed it with questions. Once it was fed with a pannel of more or less 50 questions.
I asked it to output a pannel of 100 questions in a Json format (.json) based on what I had given it.
With that done, I created:
- An HTML page: for the the website’s placement HTML Code
- A CSS script to make that page a little prettier: CSS Code
- A JavaScript script (.js) for it’s functionality: JavaScript Code
Format
For the program to recognize the Json it has to have the following format:
{
"subject": "C++",
"questions":
[
//Single choise example
{
"question": "What will be the output of this code?\n\n```cpp\n#include <iostream>\nusing namespace std;\nint main() {\n int a = 5;\n int &ref = a;\n ref = 10;\n cout << a;\n return 0;\n}\n```",
"choices": {
"A": "5",
"B": "10",
"C": "Compilation error",
"D": "Undefined behavior"
},
"correct_answer": "B",
"explanation": "The reference 'ref' refers to 'a'. Assigning 10 to 'ref' changes 'a' to 10, so output is 10."
},
//Multiple choise questions
{
"question": "Which of the following are fundamental data types in C++?",
"choices": {
"A": "int",
"B": "std::vector",
"C": "char",
"D": "double"
},
"correct_answer": ["A", "C", "D"],
"explanation": "'std::vector' is a container from the STL, not a fundamental type."
},
{"ANY OTHER QUESTION ADDED"},
{"ANY OTHER QUESTION ADDED"},
{...}
]
}
Limitations
Knowing this program/webpage was built in one day, there are multiple possibilities of improvments like:
- Saving the current session
- Giving the possibility for users to input and save their own .json files
- Adding other type of questions (order, textual answer, aso..)
Given my objectives, I do not plan on improving this tool for the moment.
Result
Once finished, the basic version allowed me to train on multiple choise questions without being limited.


Original Code
HTML Code (.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz Trainer - Interactive Learning Platform</title>
<!-- The href path assumes your CSS file is in public/quiztrainer.css -->
<link rel="stylesheet" href="/quiztrainer/quiztrainer.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<h1><i class="fas fa-brain"></i> Quiz Trainer</h1>
<p>Select a quiz from the server or upload your own JSON file to start practicing.</p>
</header>
<!-- Setup Section -->
<section id="setup-section" class="setup-section">
<div class="server-selection">
<h3><i class="fas fa-server"></i> Select a Quiz</h3>
<p>Choose from quizzes available on the server.</p>
<div class="select-container">
<select id="quiz-select">
<option value="">Loading quizzes...</option>
</select>
</div>
</div>
<div class="divider">OR</div>
<div class="upload-area" id="upload-area">
<div class="upload-content">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<h3>Upload Your Quiz File</h3>
<p>Drag and drop your JSON file here or click to browse.</p>
<input type="file" id="file-input" accept=".json" hidden>
<button class="upload-btn" onclick="document.getElementById('file-input' ).click()">
<i class="fas fa-folder-open"></i> Choose File
</button>
</div>
</div>
<div class="file-info" id="file-info" style="display: none;">
<div class="file-details">
<i class="fas fa-file-alt"></i>
<span id="file-name"></span>
</div>
<div class="quiz-options">
<div class="option-item">
<input type="checkbox" id="randomize-questions">
<label for="randomize-questions">Randomize Questions</label>
</div>
<div class="option-item">
<label for="question-amount">Number of Questions:</label>
<input type="number" id="question-amount" min="1">
<span id="question-count"></span>
</div>
</div>
<button class="start-btn" id="start-quiz">
<i class="fas fa-play"></i> Start Quiz
</button>
</div>
</section>
<!-- Contribution Section -->
<section id="contribution-section" class="contribution-section">
<div class="contribution-content">
<h3><i class="fas fa-hands-helping"></i> Want to Contribute a Quiz?</h3>
<p>
If you have created a high-quality quiz, email your <code>.json</code> file to <a href="mailto:your-email@example.com">your-email@example.com</a> for review.
</p>
</div>
</section>
<!-- Quiz Section -->
<section id="quiz-section" class="quiz-section" style="display: none;">
<div class="progress-container">
<div class="progress-info">
<span id="current-question">1</span> of <span id="total-questions">0</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="score-info">
Score: <span id="current-score">0</span>/<span id="attempted-questions">0</span>
</div>
</div>
<div class="question-card">
<div class="question-header">
<h2>Question <span id="question-number">1</span></h2>
<p id="question-type-info" class="question-type-info"></p>
</div>
<div class="question-content">
<div id="question-text"></div>
</div>
<div class="choices-container" id="choices-container"></div>
<div class="submit-container">
<button class="submit-btn" id="submit-answer-btn">
<i class="fas fa-check-double"></i> Submit Answer
</button>
</div>
<div class="feedback-section" id="feedback-section" style="display: none;">
<div class="feedback-content">
<div class="feedback-header">
<i id="feedback-icon"></i>
<span id="feedback-title"></span>
</div>
<div class="explanation" id="explanation"></div>
</div>
</div>
<div class="navigation">
<button class="nav-btn" id="prev-btn" disabled>
<i class="fas fa-chevron-left"></i> Previous
</button>
<button class="nav-btn primary" id="next-btn" disabled>
Next <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</section>
<!-- Results Section -->
<section id="results-section" class="results-section" style="display: none;">
<div class="results-card">
<div class="results-header">
<i class="fas fa-trophy"></i>
<h2>Quiz Complete!</h2>
</div>
<div class="results-content">
<div class="score-circle">
<div class="score-text"><span id="final-score">0</span>%</div>
</div>
<div class="results-details">
<div class="result-item">
<span class="label">Correct Answers:</span>
<span id="correct-count">0</span>
</div>
<div class="result-item">
<span class="label">Total Questions:</span>
<span id="total-count">0</span>
</div>
<div class="result-item">
<span class="label">Accuracy:</span>
<span id="accuracy">0%</span>
</div>
</div>
</div>
<div class="results-actions">
<button class="action-btn" id="restart-quiz">
<i class="fas fa-redo"></i> Restart Quiz
</button>
<button class="action-btn secondary" id="upload-new">
<i class="fas fa-undo"></i> Back to Setup
</button>
</div>
</div>
</section>
</div>
<!-- SCRIPTS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script defer src="quiztrainer/quiztrainer.js"></script>
</body>
</html>
CSS Code (.css)
/* Reset and Base Styles */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100% ); min-height: 100vh; color: #333; line-height: 1.6; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
/* Header */
.header { text-align: center; margin-bottom: 40px; color: white; }
.header h1 { font-size: 3rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
.header p { font-size: 1.2rem; opacity: 0.9; }
/* Setup Section */
.setup-section { background: white; border-radius: 20px; padding: 40px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 30px; }
.server-selection { text-align: center; margin-bottom: 20px; }
.server-selection h3 { font-size: 1.8rem; margin-bottom: 10px; }
.server-selection p { color: #666; margin-bottom: 20px; }
.select-container { position: relative; max-width: 500px; margin: 0 auto; }
#quiz-select { width: 100%; padding: 15px; font-size: 1.1rem; border: 2px solid #ddd; border-radius: 10px; appearance: none; -webkit-appearance: none; background-color: #f8f9fa; cursor: pointer; }
.divider { text-align: center; font-weight: bold; color: #aaa; margin: 30px 0; position: relative; }
.divider::before, .divider::after { content: ''; position: absolute; top: 50%; width: 40%; height: 1px; background: #ddd; }
.divider::before { left: 0; }
.divider::after { right: 0; }
/* Upload Section */
.upload-area { border: 3px dashed #667eea; border-radius: 15px; padding: 40px 20px; text-align: center; transition: all 0.3s ease; cursor: pointer; }
.upload-area:hover { border-color: #764ba2; background: rgba(102, 126, 234, 0.05); }
.upload-icon { font-size: 3rem; color: #667eea; margin-bottom: 15px; }
.upload-content h3 { font-size: 1.5rem; margin-bottom: 10px; }
.upload-content p { color: #666; margin-bottom: 20px; }
.upload-btn { background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 12px 25px; border-radius: 50px; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; }
.upload-btn:hover { transform: translateY(-2px); }
/* File Info & Options */
.file-info { background: #f8f9fa; padding: 20px; border-radius: 15px; margin-top: 30px; display: flex; flex-direction: column; gap: 20px; align-items: center; }
.file-details { font-weight: bold; font-size: 1.2rem; display: flex; align-items: center; gap: 10px; }
.file-details i { color: #667eea; }
.quiz-options { display: flex; gap: 30px; align-items: center; flex-wrap: wrap; justify-content: center; }
.option-item { display: flex; align-items: center; gap: 8px; }
#question-amount { width: 80px; padding: 8px 12px; border: 1px solid #ccc; border-radius: 8px; font-size: 1rem; background-color: #ffffff; color: #333333; }
#question-amount:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); }
#question-count { color: #555; }
.start-btn { background: #28a745; color: white; border: none; padding: 15px 40px; border-radius: 25px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; font-size: 1.2rem; }
.start-btn:hover { background: #218838; transform: translateY(-1px); }
/* Contribution Section */
.contribution-section { background: rgba(255, 255, 255, 0.1); border-radius: 15px; padding: 30px; margin-top: -10px; margin-bottom: 40px; text-align: center; color: white; border: 1px solid rgba(255, 255, 255, 0.2); }
.contribution-content h3 { font-size: 1.6rem; margin-bottom: 15px; }
.contribution-content p { font-size: 1.1rem; line-height: 1.7; max-width: 800px; margin: 0 auto; opacity: 0.9; }
.contribution-content code { background-color: rgba(0, 0, 0, 0.2); padding: 3px 6px; border-radius: 5px; }
.contribution-content a { color: #ffd700; font-weight: 600; text-decoration: none; }
.contribution-content a:hover { text-decoration: underline; }
/* Quiz Section */
.quiz-section { background: white; border-radius: 20px; padding: 30px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); }
.progress-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 15px; }
.progress-info, .score-info { font-weight: 600; }
.progress-bar { flex: 1; height: 8px; background: #e9ecef; border-radius: 4px; margin: 0 20px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: 0%; }
.question-card { background: white; border-radius: 15px; overflow: hidden; }
.question-header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 20px; text-align: center; }
.question-header h2 { font-size: 1.5rem; margin-bottom: 5px; }
.question-type-info { font-size: 0.9rem; opacity: 0.8; font-style: italic; }
.question-content { padding: 30px; }
#question-text { font-size: 1.2rem; line-height: 1.8; margin-bottom: 30px; }
pre { background: #2d3748 !important; border-radius: 10px !important; padding: 20px !important; margin: 20px 0 !important; overflow-x: auto; }
code { font-family: 'Fira Code', 'Consolas', monospace !important; font-size: 0.9rem !important; }
/* Choices */
.choices-container { display: grid; gap: 15px; margin-bottom: 30px; }
.choice { background: #f8f9fa; border: 2px solid #e9ecef; border-radius: 10px; padding: 15px 20px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; gap: 15px; }
.choice:hover { border-color: #667eea; background: rgba(102, 126, 234, 0.05); transform: translateY(-2px); }
.choice.selected { border-color: #667eea; background: rgba(102, 126, 234, 0.1); }
.choice.correct { border-color: #28a745; background: rgba(40, 167, 69, 0.1); }
.choice.incorrect { border-color: #dc3545; background: rgba(220, 53, 69, 0.1); }
.choice-indicator { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.2s ease; font-weight: bold; }
.choice-indicator.letter { background: #667eea; color: white; border-radius: 50%; }
.choice.correct .choice-indicator.letter { background: #28a745; }
.choice.incorrect .choice-indicator.letter { background: #dc3545; }
.choice-indicator.checkbox { border: 2px solid #aaa; border-radius: 8px; }
.choice-indicator.checkbox i { font-size: 18px; color: white; opacity: 0; transform: scale(0.5); transition: all 0.2s ease; }
.choice.selected .choice-indicator.checkbox { background-color: #667eea; border-color: #667eea; }
.choice.selected .choice-indicator.checkbox i { opacity: 1; transform: scale(1); }
.choice.correct .choice-indicator.checkbox { background-color: #28a745; border-color: #28a745; }
.choice.incorrect .choice-indicator.checkbox { background-color: #dc3545; border-color: #dc3545; }
.choice-text { flex: 1; font-size: 1.1rem; }
/* Submit Button */
.submit-container { padding: 10px 30px; text-align: center; }
.submit-btn { background: #ffc107; color: #333; border: none; padding: 15px 40px; border-radius: 50px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; }
.submit-btn:hover:not(:disabled) { transform: translateY(-2px); }
.submit-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Feedback & Navigation */
.feedback-section { background: #f8f9fa; border-radius: 10px; padding: 25px; margin-bottom: 30px; border-left: 5px solid #667eea; }
.feedback-header { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; font-weight: 600; font-size: 1.2rem; }
.feedback-section.correct { border-left-color: #28a745; background: rgba(40, 167, 69, 0.05); }
.feedback-section.incorrect { border-left-color: #dc3545; background: rgba(220, 53, 69, 0.05); }
.feedback-section.correct .feedback-header { color: #28a745; }
.feedback-section.incorrect .feedback-header { color: #dc3545; }
.explanation { color: #555; line-height: 1.6; font-size: 1.1rem; }
.navigation { display: flex; justify-content: space-between; gap: 20px; }
.nav-btn { padding: 15px 30px; border: 2px solid #667eea; background: white; color: #667eea; border-radius: 50px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; font-size: 1rem; flex: 1; max-width: 200px; }
.nav-btn:hover:not(:disabled) { background: #667eea; color: white; transform: translateY(-2px); }
.nav-btn.primary { background: #667eea; color: white; }
.nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Results Section */
.results-section { background: white; border-radius: 20px; padding: 40px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); text-align: center; }
.results-header i { font-size: 4rem; color: #ffd700; margin-bottom: 20px; }
.results-header h2 { font-size: 2.5rem; }
.results-content { display: flex; align-items: center; justify-content: center; gap: 60px; margin-bottom: 40px; flex-wrap: wrap; }
.score-circle { width: 200px; height: 200px; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: relative; }
.score-circle::before { content: ''; position: absolute; width: 160px; height: 160px; background: white; border-radius: 50%; }
.score-text { position: relative; font-size: 3rem; font-weight: bold; }
.results-details { text-align: left; }
.result-item { display: flex; justify-content: space-between; padding: 15px 0; border-bottom: 1px solid #eee; font-size: 1.2rem; }
.result-item:last-child { border-bottom: none; }
.label { font-weight: 600; color: #555; }
.results-actions { display: flex; gap: 20px; justify-content: center; margin-top: 20px; }
.action-btn { padding: 15px 30px; border: none; border-radius: 50px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; font-size: 1rem; }
.action-btn:not(.secondary) { background: linear-gradient(135deg, #667eea, #764ba2); color: white; }
.action-btn.secondary { background: #6c757d; color: white; }
.action-btn:hover { transform: translateY(-2px); }
/* Responsive */
@media (max-width: 768px) {
.header h1 { font-size: 2rem; }
.setup-section, .quiz-section, .results-section { padding: 20px; }
.progress-container { flex-direction: column; gap: 15px; }
.navigation, .results-actions { flex-direction: column; }
.nav-btn { max-width: none; }
}
JavaScript Code (.js)
// @ts-nocheck
class QuizTrainer {
constructor() {
this.allQuestions = [];
this.questions = [];
this.currentQuestionIndex = 0;
this.userAnswers = [];
this.score = 0;
this.isAnswered = false;
this.selectedChoices = [];
this.bindEventListeners();
this.fetchQuizFiles();
}
bindEventListeners() {
document.getElementById('file-input').addEventListener('change', (e) => this.handleFileSelect(e));
document.getElementById('quiz-select').addEventListener('change', (e) => this.handleQuizSelect(e));
const uploadArea = document.getElementById('upload-area');
uploadArea.addEventListener('dragover', (e) => this.handleDragOver(e));
uploadArea.addEventListener('dragleave', (e) => this.handleDragLeave(e));
uploadArea.addEventListener('drop', (e) => this.handleFileDrop(e));
document.getElementById('start-quiz').addEventListener('click', () => this.startQuiz());
document.getElementById('submit-answer-btn').addEventListener('click', () => this.submitAnswer());
document.getElementById('prev-btn').addEventListener('click', () => this.previousQuestion());
document.getElementById('next-btn').addEventListener('click', () => this.nextQuestion());
document.getElementById('restart-quiz').addEventListener('click', () => this.restartQuiz());
document.getElementById('upload-new').addEventListener('click', () => this.resetToSetup());
}
async fetchQuizFiles() {
const select = document.getElementById('quiz-select');
try {
const response = await fetch('/api/quizzes');
if (!response.ok) throw new Error(`Network response was not ok`);
const files = await response.json();
select.innerHTML = '<option value="">-- Select a quiz from the server --</option>';
files.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file.replace('.json', '').replace(/_/g, ' ');
select.appendChild(option);
});
} catch (error) {
console.error('Failed to fetch quiz files:', error);
select.innerHTML = '<option value="">Could not load server quizzes</option>';
}
}
async handleQuizSelect(e) {
const fileName = e.target.value;
document.getElementById('file-info').style.display = 'none';
this.allQuestions = [];
if (!fileName) return;
try {
const response = await fetch(`/quizzes/${fileName}`);
if (!response.ok) throw new Error(`Failed to load ${fileName}`);
const jsonData = await response.json();
this.validateAndLoadQuestions(jsonData, fileName);
document.getElementById('file-input').value = '';
} catch (error) {
this.showError(error.message);
}
}
handleDragOver(e) { e.preventDefault(); document.getElementById('upload-area').classList.add('dragover'); }
handleDragLeave(e) { e.preventDefault(); document.getElementById('upload-area').classList.remove('dragover'); }
handleFileDrop(e) {
e.preventDefault();
document.getElementById('upload-area').classList.remove('dragover');
if (e.dataTransfer.files.length > 0) this.processFile(e.dataTransfer.files[0]);
}
handleFileSelect(e) { if (e.target.files.length > 0) this.processFile(e.target.files[0]); }
processFile(file) {
if (!file.name.toLowerCase().endsWith('.json')) return this.showError('Please select a valid JSON file.');
const reader = new FileReader();
reader.onload = (e) => {
try {
const jsonData = JSON.parse(e.target.result);
this.validateAndLoadQuestions(jsonData, file.name);
document.getElementById('quiz-select').value = '';
} catch (error) {
this.showError('Invalid JSON file. Please check the file format.');
}
};
reader.readAsText(file);
}
validateAndLoadQuestions(data, fileName) {
const isValid = Array.isArray(data) && data.length > 0 && data.every(q => q.question && q.choices && q.correct_answer && q.explanation);
if (!isValid) return this.showError(`Invalid JSON format in "${fileName}".`);
this.allQuestions = data;
this.showFileInfo(fileName, data.length);
}
showFileInfo(fileName, count) {
document.getElementById('file-name').textContent = fileName;
const amountInput = document.getElementById('question-amount');
amountInput.max = count;
amountInput.value = count;
document.getElementById('question-count').textContent = `/ ${count}`;
document.getElementById('file-info').style.display = 'flex';
}
prepareQuizQuestions() {
let prepared = [...this.allQuestions];
if (document.getElementById('randomize-questions').checked) {
prepared.sort(() => Math.random() - 0.5);
}
const amount = parseInt(document.getElementById('question-amount').value, 10);
if (!isNaN(amount) && amount > 0 && amount < prepared.length) {
prepared = prepared.slice(0, amount);
}
this.questions = prepared;
}
startQuiz() {
if (this.allQuestions.length === 0) return this.showError("No quiz loaded.");
this.prepareQuizQuestions();
if (this.questions.length === 0) return this.showError("Invalid number of questions.");
this.currentQuestionIndex = 0;
this.userAnswers = new Array(this.questions.length).fill(null);
this.score = 0;
document.getElementById('setup-section').style.display = 'none';
document.getElementById('contribution-section').style.display = 'none';
document.getElementById('quiz-section').style.display = 'block';
document.getElementById('results-section').style.display = 'none';
this.displayQuestion();
}
displayQuestion() {
this.selectedChoices = [];
this.isAnswered = false;
const question = this.questions[this.currentQuestionIndex];
const userAnswer = this.userAnswers[this.currentQuestionIndex];
document.getElementById('question-number').textContent = this.currentQuestionIndex + 1;
const isMultiChoice = Array.isArray(question.correct_answer);
document.getElementById('question-type-info').textContent = isMultiChoice ? "(Select all that apply)" : "(Select one answer)";
const questionElement = document.getElementById('question-text');
questionElement.innerHTML = this.formatQuestionText(question.question);
Prism.highlightAllUnder(questionElement);
this.displayChoices(question, userAnswer);
if (userAnswer !== null) {
this.isAnswered = true;
const isCorrect = this.validateAnswer(userAnswer, question.correct_answer);
this.showFeedback(isCorrect, question.explanation);
} else {
this.hideFeedback();
}
this.updateQuizInfo();
this.updateNavigation();
}
displayChoices(question, userAnswer) {
const { choices, correct_answer } = question;
const container = document.getElementById('choices-container');
container.innerHTML = '';
const isMultiChoice = Array.isArray(correct_answer);
Object.entries(choices).forEach(([letter, text]) => {
const choiceElement = document.createElement('div');
choiceElement.className = 'choice';
choiceElement.dataset.choice = letter;
if (userAnswer !== null) {
const correctAnswers = isMultiChoice ? correct_answer : [correct_answer];
const isCorrectChoice = correctAnswers.includes(letter);
const wasSelected = userAnswer.includes(letter);
if (isCorrectChoice) choiceElement.classList.add('correct');
if (wasSelected && !isCorrectChoice) choiceElement.classList.add('incorrect');
if (wasSelected) choiceElement.classList.add('selected');
}
const indicatorType = isMultiChoice ? 'checkbox' : 'letter';
const indicatorContent = isMultiChoice ? '<i class="fas fa-check"></i>' : letter;
choiceElement.innerHTML = `
<div class="choice-indicator ${indicatorType}">${indicatorContent}</div>
<div class="choice-text">${this.escapeHtml(text)}</div>
`;
if (userAnswer === null) {
choiceElement.addEventListener('click', () => this.handleChoiceClick(letter, isMultiChoice));
}
container.appendChild(choiceElement);
});
}
handleChoiceClick(selectedChoice, isMultiChoice) {
if (this.isAnswered) return;
if (isMultiChoice) {
const choiceElement = document.querySelector(`[data-choice="${selectedChoice}"]`);
const index = this.selectedChoices.indexOf(selectedChoice);
if (index > -1) {
this.selectedChoices.splice(index, 1);
choiceElement.classList.remove('selected');
} else {
this.selectedChoices.push(selectedChoice);
choiceElement.classList.add('selected');
}
document.getElementById('submit-answer-btn').disabled = this.selectedChoices.length === 0;
} else {
this.selectedChoices = [selectedChoice];
this.submitAnswer();
}
}
submitAnswer() {
if (this.isAnswered || this.selectedChoices.length === 0) return;
const question = this.questions[this.currentQuestionIndex];
const userSelection = [...this.selectedChoices].sort();
this.userAnswers[this.currentQuestionIndex] = userSelection;
const isCorrect = this.validateAnswer(userSelection, question.correct_answer);
if (isCorrect) this.score++;
this.isAnswered = true;
this.displayChoices(question, userSelection);
this.showFeedback(isCorrect, question.explanation);
this.updateQuizInfo();
this.updateNavigation();
}
validateAnswer(userSelection, correctAnswers) {
if (Array.isArray(correctAnswers)) {
// Multi-choice validation
return userSelection.length === correctAnswers.length && userSelection.every(choice => correctAnswers.includes(choice));
}
// Single-choice validation
return userSelection.length === 1 && userSelection[0] === correctAnswers;
}
updateQuizInfo() {
document.getElementById('current-question').textContent = this.currentQuestionIndex + 1;
document.getElementById('total-questions').textContent = this.questions.length;
const progress = ((this.currentQuestionIndex + 1) / this.questions.length) * 100;
document.getElementById('progress-fill').style.width = `${progress}%`;
const attemptedCount = this.userAnswers.filter(answer => answer !== null).length;
document.getElementById('current-score').textContent = this.score;
document.getElementById('attempted-questions').textContent = attemptedCount;
}
updateNavigation() {
const question = this.questions[this.currentQuestionIndex];
const isMultiChoice = Array.isArray(question.correct_answer);
const submitBtn = document.getElementById('submit-answer-btn');
submitBtn.style.display = (isMultiChoice && !this.isAnswered) ? 'inline-block' : 'none';
submitBtn.disabled = this.selectedChoices.length === 0;
document.getElementById('prev-btn').disabled = this.currentQuestionIndex === 0;
const nextBtn = document.getElementById('next-btn');
nextBtn.disabled = !this.isAnswered;
nextBtn.innerHTML = (this.currentQuestionIndex === this.questions.length - 1) ? 'Finish Quiz <i class="fas fa-flag-checkered"></i>' : 'Next <i class="fas fa-chevron-right"></i>';
}
// --- START OF MISSING FUNCTIONS ---
showFeedback(isCorrect, explanation) {
const feedbackSection = document.getElementById('feedback-section');
const feedbackIcon = document.getElementById('feedback-icon');
const feedbackTitle = document.getElementById('feedback-title');
const explanationElement = document.getElementById('explanation');
feedbackSection.style.display = 'block';
feedbackSection.className = `feedback-section ${isCorrect ? 'correct' : 'incorrect'}`;
feedbackIcon.className = isCorrect ? 'fas fa-check-circle' : 'fas fa-times-circle';
feedbackTitle.textContent = isCorrect ? 'Correct!' : 'Incorrect';
explanationElement.textContent = explanation;
}
hideFeedback() {
document.getElementById('feedback-section').style.display = 'none';
}
// --- END OF MISSING FUNCTIONS ---
previousQuestion() {
if (this.currentQuestionIndex > 0) {
this.currentQuestionIndex--;
this.displayQuestion();
}
}
nextQuestion() {
if (this.currentQuestionIndex < this.questions.length - 1) {
this.currentQuestionIndex++;
this.displayQuestion();
} else {
this.finishQuiz();
}
}
finishQuiz() {
document.getElementById('quiz-section').style.display = 'none';
document.getElementById('results-section').style.display = 'block';
const total = this.questions.length;
const percentage = total > 0 ? Math.round((this.score / total) * 100) : 0;
document.getElementById('final-score').textContent = percentage;
document.getElementById('correct-count').textContent = this.score;
document.getElementById('total-count').textContent = total;
document.getElementById('accuracy').textContent = `${percentage}%`;
this.animateScoreCircle(percentage);
}
animateScoreCircle(percentage) {
const circle = document.querySelector('.score-circle');
const degrees = (percentage / 100) * 360;
let color1 = '#dc3545', color2 = '#e83e8c';
if (percentage >= 80) { color1 = '#28a745'; color2 = '#20c997'; }
else if (percentage >= 50) { color1 = '#ffc107'; color2 = '#fd7e14'; }
circle.style.background = `conic-gradient(${color1} 0deg, ${color2} ${degrees}deg, #e9ecef ${degrees}deg, #e9ecef 360deg)`;
}
restartQuiz() { this.startQuiz(); }
resetToSetup() {
document.getElementById('setup-section').style.display = 'block';
document.getElementById('contribution-section').style.display = 'block';
document.getElementById('quiz-section').style.display = 'none';
document.getElementById('results-section').style.display = 'none';
document.getElementById('file-info').style.display = 'none';
document.getElementById('file-input').value = '';
document.getElementById('quiz-select').value = '';
this.allQuestions = [];
}
showError(message) { alert(`Error: ${message}`); }
formatQuestionText(text) {
return text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const language = lang || 'plaintext';
return `<pre><code class="language-${language}">${this.escapeHtml(code.trim())}</code></pre>`;
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize the application now that the DOM is ready (due to the 'defer' attribute on the script tag)
new QuizTrainer();