mcq / app.py
rsalehin's picture
Update app.py
ccac3b3 verified
import gradio as gr
import json
import xml.etree.ElementTree as ET
import os
from typing import List, Dict, Any
class QuizApp:
def __init__(self):
self.questions = []
self.current_quiz = None
def parse_json_file(self, file_path: str) -> List[Dict[str, Any]]:
"""Parse JSON file containing quiz questions"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
questions = []
# Handle different JSON structures
if isinstance(data, list):
questions = data
elif isinstance(data, dict):
if 'questions' in data:
questions = data['questions']
else:
questions = [data]
# Validate question format
validated_questions = []
for q in questions:
if all(key in q for key in ['question', 'options', 'answer']):
validated_questions.append({
'question': q['question'],
'options': q['options'] if isinstance(q['options'], list) else [q['options']],
'answer': q['answer']
})
return validated_questions
except Exception as e:
raise Exception(f"Error parsing JSON file: {str(e)}")
def parse_xml_file(self, file_path: str) -> List[Dict[str, Any]]:
"""Parse XML file containing quiz questions"""
try:
tree = ET.parse(file_path)
root = tree.getroot()
questions = []
# Handle different XML structures
question_elements = root.findall('.//question') or root.findall('.//item')
for q_elem in question_elements:
question_text = q_elem.find('text') or q_elem.find('question')
options_elem = q_elem.find('options') or q_elem.find('choices')
answer_elem = q_elem.find('answer') or q_elem.find('correct')
if question_text is not None and options_elem is not None and answer_elem is not None:
options = []
for option in options_elem.findall('option') or options_elem.findall('choice'):
if option.text:
options.append(option.text.strip())
if len(options) >= 2: # At least 2 options required
questions.append({
'question': question_text.text.strip(),
'options': options,
'answer': answer_elem.text.strip()
})
return questions
except Exception as e:
raise Exception(f"Error parsing XML file: {str(e)}")
def load_quiz_file(self, file) -> str:
"""Load and parse quiz file"""
if file is None:
return "Please upload a file first."
try:
file_path = file.name
file_extension = os.path.splitext(file_path)[1].lower()
if file_extension == '.json':
self.questions = self.parse_json_file(file_path)
elif file_extension == '.xml':
self.questions = self.parse_xml_file(file_path)
else:
return "Unsupported file format. Please upload a JSON or XML file."
if not self.questions:
return "No valid questions found in the file. Please check the file format."
return f"βœ… Successfully loaded {len(self.questions)} questions! Click 'Start Quiz' to begin."
except Exception as e:
return f"❌ Error loading file: {str(e)}"
def generate_quiz_html(self) -> str:
"""Generate HTML for the quiz interface"""
if not self.questions:
return "<p>No questions loaded. Please upload a quiz file first.</p>"
# Create JSON data for JavaScript
quiz_data_json = json.dumps(self.questions).replace('"', '&quot;')
html = f"""
<div style="max-width: 800px; margin: 0 auto; padding: 20px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; text-align: center; margin-bottom: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);">
<h1 style="margin: 0; font-size: 2.5em; font-weight: 300;">πŸ“ Multiple Choice Quiz</h1>
<p style="margin: 10px 0 0 0; font-size: 1.2em; opacity: 0.9;">Test your knowledge with {len(self.questions)} questions</p>
</div>
<form id="quizForm" style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1);">
"""
for i, q in enumerate(self.questions):
html += f"""
<div style="margin-bottom: 35px; padding: 25px; background: #f8f9ff; border-radius: 12px; border-left: 5px solid #667eea;">
<h3 style="color: #333; margin-bottom: 20px; font-size: 1.3em; line-height: 1.4;">
<span style="background: #667eea; color: white; padding: 5px 12px; border-radius: 20px; font-size: 0.8em; margin-right: 10px;">Q{i+1}</span>
{q['question']}
</h3>
<div style="margin-left: 10px;">
"""
for j, option in enumerate(q['options']):
option_letter = chr(65 + j) # A, B, C, D
html += f"""
<label style="display: block; margin-bottom: 12px; padding: 12px 15px; background: white; border: 2px solid #e1e5e9; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; font-size: 1.1em;"
onmouseover="this.style.borderColor='#667eea'; this.style.backgroundColor='#f0f3ff';"
onmouseout="this.style.borderColor='#e1e5e9'; this.style.backgroundColor='white';"
onclick="selectAnswer(this)">
<input type="radio" name="q{i}" value="{option}" style="margin-right: 12px; transform: scale(1.2);">
<span style="font-weight: 500; color: #667eea; margin-right: 8px;">{option_letter}.</span>
<span style="color: #333;">{option}</span>
</label>
"""
html += """
</div>
</div>
"""
html += f"""
<div style="text-align: center; margin-top: 40px;">
<button type="button" onclick="submitQuiz()"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 15px 40px; font-size: 1.2em; border-radius: 30px; cursor: pointer; box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); transition: all 0.3s ease; font-weight: 500;"
onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 25px rgba(102, 126, 234, 0.6)';"
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 5px 15px rgba(102, 126, 234, 0.4)';">
πŸš€ Submit Quiz
</button>
</div>
</form>
<div id="results" style="margin-top: 30px; display: none;"></div>
</div>
<script>
// Store quiz data
const quizData = JSON.parse("{quiz_data_json}".replace(/&quot;/g, '"'));
function selectAnswer(labelElement) {{
// Remove previous selections from this question
const questionDiv = labelElement.closest('div[style*="margin-bottom: 35px"]');
const labels = questionDiv.querySelectorAll('label');
labels.forEach(label => {{
label.style.borderColor = '#e1e5e9';
label.style.backgroundColor = 'white';
}});
// Highlight selected answer
labelElement.style.borderColor = '#667eea';
labelElement.style.backgroundColor = '#e8f0fe';
}}
function submitQuiz() {{
console.log('Submit button clicked');
console.log('Quiz data:', quizData);
const form = document.getElementById('quizForm');
const answers = {{}};
let totalQuestions = {len(self.questions)};
let answeredQuestions = 0;
// Collect answers
for (let i = 0; i < totalQuestions; i++) {{
const radioButtons = document.getElementsByName(`q${{i}}`);
for (let radio of radioButtons) {{
if (radio.checked) {{
answers[`q${{i}}`] = radio.value;
answeredQuestions++;
break;
}}
}}
}}
console.log('Collected answers:', answers);
console.log('Answered questions:', answeredQuestions);
// Check if all questions are answered
if (answeredQuestions < totalQuestions) {{
alert(`⚠️ Please answer all questions before submitting! You have answered ${{answeredQuestions}} out of ${{totalQuestions}} questions.`);
return;
}}
// Calculate score
let score = 0;
let feedback = [];
quizData.forEach((question, index) => {{
const userAnswer = answers[`q${{index}}`];
const correctAnswer = question.answer;
const isCorrect = userAnswer === correctAnswer;
if (isCorrect) {{
score++;
}}
feedback.push({{
question: question.question,
userAnswer: userAnswer || 'No answer',
correctAnswer: correctAnswer,
isCorrect: isCorrect
}});
}});
console.log('Final score:', score);
console.log('Feedback:', feedback);
displayResults(score, totalQuestions, feedback);
}}
function displayResults(score, total, feedback) {{
const percentage = Math.round((score / total) * 100);
let resultsHtml = `
<div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1);">
<div style="text-align: center; margin-bottom: 30px;">
<h2 style="color: #333; margin-bottom: 15px;">πŸŽ‰ Quiz Complete!</h2>
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 15px; display: inline-block; min-width: 200px;">
<div style="font-size: 3em; font-weight: bold; margin-bottom: 10px;">${{score}}/${{total}}</div>
<div style="font-size: 1.5em;">${{percentage}}%</div>
</div>
</div>
<div style="margin-top: 30px;">
<h3 style="color: #333; margin-bottom: 20px; text-align: center;">πŸ“Š Detailed Results</h3>
`;
feedback.forEach((item, index) => {{
const icon = item.isCorrect ? 'βœ…' : '❌';
const bgColor = item.isCorrect ? '#e8f5e8' : '#ffe8e8';
const borderColor = item.isCorrect ? '#4caf50' : '#f44336';
resultsHtml += `
<div style="margin-bottom: 20px; padding: 20px; background: ${{bgColor}}; border-left: 5px solid ${{borderColor}}; border-radius: 8px;">
<div style="font-weight: bold; color: #333; margin-bottom: 10px;">
${{icon}} Question ${{index + 1}}
</div>
<div style="color: #666; margin-bottom: 10px; font-style: italic;">
"${{item.question}}"
</div>
<div style="margin-bottom: 5px;">
<strong>Your answer:</strong> ${{item.userAnswer}}
</div>
<div>
<strong>Correct answer:</strong> ${{item.correctAnswer}}
</div>
</div>
`;
}});
resultsHtml += `
</div>
<div style="text-align: center; margin-top: 30px;">
<button onclick="location.reload()"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 30px; font-size: 1.1em; border-radius: 25px; cursor: pointer; box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);">
πŸ”„ Take Quiz Again
</button>
</div>
</div>
`;
document.getElementById('results').innerHTML = resultsHtml;
document.getElementById('results').style.display = 'block';
document.getElementById('quizForm').style.display = 'none';
// Scroll to results
document.getElementById('results').scrollIntoView({{ behavior: 'smooth' }});
}}
// Add some debugging
console.log('Quiz script loaded');
console.log('Quiz data available:', typeof quizData !== 'undefined');
</script>
"""
return html
# Initialize the quiz app
quiz_app = QuizApp()
# Create Gradio interface
with gr.Blocks(
title="Multiple Choice Quiz App",
theme=gr.themes.Soft(),
css="""
.gradio-container {
max-width: 1200px !important;
margin: auto !important;
}
.upload-area {
border: 2px dashed #667eea !important;
border-radius: 10px !important;
background: #f8f9ff !important;
}
"""
) as app:
gr.Markdown("""
# πŸŽ“ Multiple Choice Quiz Application
Upload your quiz file (JSON or XML format) and start taking the quiz!
### Expected File Formats:
**JSON Format:**
```json
[
{
"question": "What is the capital of France?",
"options": ["London", "Berlin", "Paris", "Madrid"],
"answer": "Paris"
}
]
```
**XML Format:**
```xml
<quiz>
<question>
<text>What is the capital of France?</text>
<options>
<option>London</option>
<option>Berlin</option>
<option>Paris</option>
<option>Madrid</option>
</options>
<answer>Paris</answer>
</question>
</quiz>
```
""")
with gr.Row():
with gr.Column(scale=1):
file_upload = gr.File(
label="πŸ“ Upload Quiz File",
file_types=[".json", ".xml"],
elem_classes=["upload-area"]
)
load_btn = gr.Button(
"πŸ“€ Load Quiz",
variant="primary",
size="lg"
)
status_msg = gr.Textbox(
label="Status",
interactive=False,
value="Please upload a quiz file to get started."
)
start_btn = gr.Button(
"πŸš€ Start Quiz",
variant="secondary",
size="lg",
visible=False
)
with gr.Column(scale=2):
quiz_html = gr.HTML(
value="<div style='text-align: center; padding: 50px; color: #666;'><h3>Upload a quiz file to begin</h3></div>"
)
# Event handlers
def load_quiz_handler(file):
result = quiz_app.load_quiz_file(file)
if "Successfully loaded" in result:
return result, gr.Button(visible=True)
else:
return result, gr.Button(visible=False)
def start_quiz_handler():
return quiz_app.generate_quiz_html()
load_btn.click(
fn=load_quiz_handler,
inputs=[file_upload],
outputs=[status_msg, start_btn]
)
start_btn.click(
fn=start_quiz_handler,
outputs=[quiz_html]
)
# Launch the app
if __name__ == "__main__":
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=True
)