|
|
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 = [] |
|
|
|
|
|
if isinstance(data, list): |
|
|
questions = data |
|
|
elif isinstance(data, dict): |
|
|
if 'questions' in data: |
|
|
questions = data['questions'] |
|
|
else: |
|
|
questions = [data] |
|
|
|
|
|
|
|
|
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 = [] |
|
|
|
|
|
|
|
|
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: |
|
|
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>" |
|
|
|
|
|
|
|
|
quiz_data_json = json.dumps(self.questions).replace('"', '"') |
|
|
|
|
|
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) |
|
|
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(/"/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 |
|
|
|
|
|
|
|
|
quiz_app = QuizApp() |
|
|
|
|
|
|
|
|
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>" |
|
|
) |
|
|
|
|
|
|
|
|
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] |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
app.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=True |
|
|
) |