First commit
Browse files- .gitignore +2 -0
- app.py +151 -0
- backend.py +23 -0
- dockerfile +29 -0
- questions/CLF-C02-v1.json +0 -0
- requirements.txt +3 -0
- static/script.js +116 -0
- static/style.css +26 -0
- templates/client.html +44 -0
- templates/host.html +39 -0
- templates/index.html +16 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
/.venv
|
app.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request
|
| 2 |
+
from flask_socketio import SocketIO, emit, join_room, leave_room
|
| 3 |
+
import backend # Import backend functions
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import base64
|
| 6 |
+
from io import BytesIO
|
| 7 |
+
import random
|
| 8 |
+
|
| 9 |
+
app = Flask(__name__)
|
| 10 |
+
app.config['SECRET_KEY'] = 'your_secret_key'
|
| 11 |
+
socketio = SocketIO(app)
|
| 12 |
+
|
| 13 |
+
exams = backend.load_question_sets() # Load available exams
|
| 14 |
+
selected_questions = [] # Global variable to store the selected questions
|
| 15 |
+
current_question = {"index": 0, "answers": {}, "started": False}
|
| 16 |
+
participants = {}
|
| 17 |
+
|
| 18 |
+
@app.route('/')
|
| 19 |
+
def index():
|
| 20 |
+
return render_template('index.html')
|
| 21 |
+
|
| 22 |
+
@app.route('/client')
|
| 23 |
+
def client():
|
| 24 |
+
return render_template('client.html')
|
| 25 |
+
|
| 26 |
+
@app.route('/host')
|
| 27 |
+
def host():
|
| 28 |
+
return render_template('host.html', exams=exams)
|
| 29 |
+
|
| 30 |
+
@socketio.on('join')
|
| 31 |
+
def on_join(data):
|
| 32 |
+
username = data['username']
|
| 33 |
+
user_id_number = random.randint(1000, 9999)
|
| 34 |
+
participants[request.sid] = {"user_id_number": user_id_number, "username": username, "score": 0}
|
| 35 |
+
join_room('quiz')
|
| 36 |
+
emit('update_participants', {"participants": participants, "count": len(participants)}, room='quiz')
|
| 37 |
+
print(f"{username} (ID: {user_id_number}) joined the quiz.")
|
| 38 |
+
|
| 39 |
+
@socketio.on('disconnect')
|
| 40 |
+
def on_leave():
|
| 41 |
+
if request.sid in participants:
|
| 42 |
+
username = participants[request.sid]["username"]
|
| 43 |
+
leave_room('quiz')
|
| 44 |
+
del participants[request.sid]
|
| 45 |
+
emit('update_participants', {"participants": participants, "count": len(participants)}, room='quiz')
|
| 46 |
+
print(f"{username} left the quiz.")
|
| 47 |
+
|
| 48 |
+
@socketio.on('load_quiz')
|
| 49 |
+
def load_quiz(data):
|
| 50 |
+
global selected_questions
|
| 51 |
+
exam_name = data['exam_name']
|
| 52 |
+
start_question = data['start_question'] - 1 # Adjust for 0-based indexing
|
| 53 |
+
selected_questions = backend.select_exam(exam_name)
|
| 54 |
+
if selected_questions:
|
| 55 |
+
num_questions = len(selected_questions)
|
| 56 |
+
current_question['index'] = start_question
|
| 57 |
+
emit('quiz_loaded', {"success": True, "num_questions": num_questions, "start_question": start_question + 1}, room=request.sid)
|
| 58 |
+
else:
|
| 59 |
+
emit('quiz_loaded', {"success": False}, room=request.sid)
|
| 60 |
+
|
| 61 |
+
@socketio.on('start_quiz')
|
| 62 |
+
def start_quiz():
|
| 63 |
+
if participants and selected_questions:
|
| 64 |
+
current_question['started'] = True
|
| 65 |
+
emit('new_question', selected_questions[current_question['index']], room='quiz')
|
| 66 |
+
# Also emit the question to the host
|
| 67 |
+
emit('new_question', selected_questions[current_question['index']], room=request.sid)
|
| 68 |
+
emit('enable_end_quiz', room=request.sid) # Enable "End Quiz" for the host
|
| 69 |
+
|
| 70 |
+
@socketio.on('restart_quiz')
|
| 71 |
+
def restart_quiz():
|
| 72 |
+
reset_quiz()
|
| 73 |
+
emit('quiz_reset', room='quiz')
|
| 74 |
+
start_quiz()
|
| 75 |
+
|
| 76 |
+
@socketio.on('submit_answer')
|
| 77 |
+
def receive_answer(data):
|
| 78 |
+
username = participants[request.sid]["username"]
|
| 79 |
+
answer = data['answer']
|
| 80 |
+
current_question['answers'][username] = answer
|
| 81 |
+
print(f"{username} submitted an answer: {answer}")
|
| 82 |
+
|
| 83 |
+
@socketio.on('check_answers')
|
| 84 |
+
def check_answers():
|
| 85 |
+
index = current_question['index']
|
| 86 |
+
if index < len(selected_questions):
|
| 87 |
+
question = selected_questions[index]
|
| 88 |
+
correct_answer = question['correct']
|
| 89 |
+
results = {
|
| 90 |
+
"question": question["question"],
|
| 91 |
+
"answers": current_question["answers"],
|
| 92 |
+
"correct_answer": correct_answer
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
chart_base64 = generate_chart(current_question["answers"], question["options"])
|
| 96 |
+
emit('display_results', {"results": results, "chart": chart_base64}, room='quiz')
|
| 97 |
+
|
| 98 |
+
for sid, participant in participants.items():
|
| 99 |
+
if current_question['answers'].get(participant["username"]) == correct_answer:
|
| 100 |
+
participants[sid]["score"] += 1
|
| 101 |
+
|
| 102 |
+
@socketio.on('next_question')
|
| 103 |
+
def next_question():
|
| 104 |
+
current_question['index'] += 1
|
| 105 |
+
current_question['answers'] = {}
|
| 106 |
+
if current_question['index'] < len(selected_questions):
|
| 107 |
+
question = selected_questions[current_question['index']]
|
| 108 |
+
emit('clear_results', room='quiz')
|
| 109 |
+
emit('new_question', question, room='quiz')
|
| 110 |
+
# Also emit the question to the host
|
| 111 |
+
emit('new_question', question, room=request.sid)
|
| 112 |
+
else:
|
| 113 |
+
final_results = calculate_final_results()
|
| 114 |
+
emit('display_final_results', final_results, room='quiz')
|
| 115 |
+
|
| 116 |
+
@socketio.on('end_quiz')
|
| 117 |
+
def end_quiz():
|
| 118 |
+
if current_question['started']: # Ensure the quiz has started before ending it
|
| 119 |
+
final_results = calculate_final_results()
|
| 120 |
+
emit('display_final_results', final_results, room='quiz')
|
| 121 |
+
reset_quiz() # Reset the quiz state
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def generate_chart(answers, options):
|
| 125 |
+
letters = [chr(65 + i) for i in range(len(options))] # Dynamically generate letters for options
|
| 126 |
+
counts = [list(answers.values()).count(option) for option in options]
|
| 127 |
+
plt.figure(figsize=(6, 4))
|
| 128 |
+
plt.bar(letters, counts)
|
| 129 |
+
plt.xlabel('Options')
|
| 130 |
+
plt.ylabel('Number of Votes')
|
| 131 |
+
plt.title('Results')
|
| 132 |
+
buf = BytesIO()
|
| 133 |
+
plt.savefig(buf, format='png')
|
| 134 |
+
buf.seek(0)
|
| 135 |
+
chart_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 136 |
+
buf.close()
|
| 137 |
+
plt.close()
|
| 138 |
+
return chart_base64
|
| 139 |
+
|
| 140 |
+
def calculate_final_results():
|
| 141 |
+
sorted_scores = sorted(participants.values(), key=lambda x: x['score'], reverse=True)
|
| 142 |
+
return [{"username": p["username"], "score": p["score"]} for p in sorted_scores]
|
| 143 |
+
|
| 144 |
+
def reset_quiz():
|
| 145 |
+
global selected_questions, current_question
|
| 146 |
+
current_question = {"index": 0, "answers": {}, "started": False}
|
| 147 |
+
for participant in participants.values():
|
| 148 |
+
participant["score"] = 0
|
| 149 |
+
|
| 150 |
+
if __name__ == '__main__':
|
| 151 |
+
socketio.run(app, host='0.0.0.0', port=7860, debug=True)
|
backend.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
# Function to load question sets from the "questions" directory
|
| 5 |
+
def load_question_sets(directory='questions'):
|
| 6 |
+
question_sets = []
|
| 7 |
+
for root, dirs, files in os.walk(directory):
|
| 8 |
+
for file in files:
|
| 9 |
+
if file.endswith(".json"):
|
| 10 |
+
question_sets.append(file[:-5]) # Remove the ".json" extension
|
| 11 |
+
return question_sets
|
| 12 |
+
|
| 13 |
+
# Function to select and load the specified exam
|
| 14 |
+
def select_exam(exam_name):
|
| 15 |
+
file_path = os.path.join('questions', f'{exam_name}.json')
|
| 16 |
+
try:
|
| 17 |
+
with open(file_path, 'r') as f:
|
| 18 |
+
questions = json.load(f)
|
| 19 |
+
print(f"Loaded {len(questions)} questions from {exam_name}")
|
| 20 |
+
return questions
|
| 21 |
+
except FileNotFoundError:
|
| 22 |
+
print(f"File {file_path} not found.")
|
| 23 |
+
return [] # Return an empty list if the file is not found
|
dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Python image with version 3.9
|
| 2 |
+
FROM python:3.9
|
| 3 |
+
|
| 4 |
+
# Create a new user to run the application
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
|
| 7 |
+
# Set the user for subsequent commands
|
| 8 |
+
USER user
|
| 9 |
+
|
| 10 |
+
# Update the PATH environment variable
|
| 11 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Set the working directory
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Copy the requirements.txt file into the container
|
| 17 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 18 |
+
|
| 19 |
+
# Install the dependencies listed in requirements.txt
|
| 20 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 21 |
+
|
| 22 |
+
# Copy the entire project into the container
|
| 23 |
+
COPY --chown=user . /app
|
| 24 |
+
|
| 25 |
+
# Expose the application port (7860) for Hugging Face Spaces
|
| 26 |
+
EXPOSE 7860
|
| 27 |
+
|
| 28 |
+
# Set the command to run the application using Uvicorn
|
| 29 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
questions/CLF-C02-v1.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask
|
| 2 |
+
flask-socketio
|
| 3 |
+
matplotlib
|
static/script.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const socket = io();
|
| 2 |
+
let username;
|
| 3 |
+
|
| 4 |
+
function joinQuiz() {
|
| 5 |
+
username = document.getElementById('username').value;
|
| 6 |
+
socket.emit('join', { username: username });
|
| 7 |
+
document.getElementById('username').style.display = 'none';
|
| 8 |
+
document.querySelector('button').style.display = 'none';
|
| 9 |
+
document.getElementById('logged-user').textContent = username;
|
| 10 |
+
document.getElementById('quiz-content').style.display = 'block';
|
| 11 |
+
document.getElementById('waiting-message').style.display = 'block';
|
| 12 |
+
document.getElementById('join-title').style.display = 'none';
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function submitForm(event) {
|
| 16 |
+
event.preventDefault();
|
| 17 |
+
const selectedOption = document.querySelector('input[name="answer"]:checked');
|
| 18 |
+
if (selectedOption) {
|
| 19 |
+
const answer = selectedOption.value;
|
| 20 |
+
socket.emit('submit_answer', { answer });
|
| 21 |
+
} else {
|
| 22 |
+
alert("Please select an option before submitting.");
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function selectExam() {
|
| 27 |
+
const examName = document.getElementById('exam-selector').value;
|
| 28 |
+
const startQuestion = document.getElementById('start-question-number').value;
|
| 29 |
+
document.getElementById('question-start-display').textContent = `Starting from question ${startQuestion}.`;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function loadQuiz() {
|
| 33 |
+
const examName = document.getElementById('exam-selector').value;
|
| 34 |
+
const startQuestion = document.getElementById('start-question-number').value;
|
| 35 |
+
socket.emit('load_quiz', { exam_name: examName, start_question: parseInt(startQuestion) });
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function updateSliderValue(value) {
|
| 39 |
+
document.getElementById('start-question').value = value;
|
| 40 |
+
document.getElementById('start-question-number').value = value;
|
| 41 |
+
document.getElementById('question-start-display').textContent = `Starting from question ${value}.`;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function startQuiz() {
|
| 45 |
+
socket.emit('start_quiz');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function checkAnswers() {
|
| 49 |
+
socket.emit('check_answers');
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function nextQuestion() {
|
| 53 |
+
socket.emit('next_question');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function endQuiz() {
|
| 57 |
+
socket.emit('end_quiz');
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function restartQuiz() {
|
| 61 |
+
socket.emit('restart_quiz');
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
socket.on('quiz_loaded', (data) => {
|
| 65 |
+
if (data.success) {
|
| 66 |
+
alert(`Quiz loaded with ${data.num_questions} questions, starting from question ${data.start_question}.`);
|
| 67 |
+
} else {
|
| 68 |
+
alert(`Failed to load quiz.`);
|
| 69 |
+
}
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
socket.on('new_question', (data) => {
|
| 73 |
+
document.getElementById('waiting-message').style.display = 'none';
|
| 74 |
+
document.getElementById('question-text').innerText = data.question;
|
| 75 |
+
// Dynamically generate letters for options (up to 'h')
|
| 76 |
+
const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
| 77 |
+
const options = data.options.map((opt, index) =>
|
| 78 |
+
`<input type="radio" id="${letters[index]}" name="answer" value="${opt}">
|
| 79 |
+
<label for="${letters[index]}">${letters[index]}) ${opt}</label><br>`
|
| 80 |
+
).join('');
|
| 81 |
+
document.getElementById('options').innerHTML = options;
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
socket.on('display_results', (data) => {
|
| 85 |
+
const img = `<img src="data:image/png;base64,${data.chart}" alt="Results Chart" />`;
|
| 86 |
+
const resultText = `<p>Correct Answer: ${data.results.correct_answer}</p>`;
|
| 87 |
+
document.getElementById('results').innerHTML = img + resultText;
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
socket.on('enable_end_quiz', () => {
|
| 91 |
+
document.getElementById('end-quiz').disabled = false; // Enable the "End Quiz" button
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
socket.on('clear_results', () => {
|
| 95 |
+
document.getElementById('results').innerHTML = '';
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
socket.on('display_final_results', (finalResults) => {
|
| 99 |
+
document.getElementById('quiz-content').style.display = 'none';
|
| 100 |
+
const resultsTable = document.getElementById('results-table');
|
| 101 |
+
resultsTable.innerHTML = '';
|
| 102 |
+
finalResults.forEach((participant) => {
|
| 103 |
+
const row = `<tr><td>${participant.username}</td><td>${participant.score}</td></tr>`;
|
| 104 |
+
resultsTable.innerHTML += row;
|
| 105 |
+
});
|
| 106 |
+
document.getElementById('final-results').style.display = 'block';
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
socket.on('quiz_reset', () => {
|
| 110 |
+
document.getElementById('results').innerHTML = '';
|
| 111 |
+
document.getElementById('question-text').innerText = '';
|
| 112 |
+
document.getElementById('options').innerHTML = '';
|
| 113 |
+
document.getElementById('final-results').style.display = 'none';
|
| 114 |
+
document.getElementById('quiz-content').style.display = 'block';
|
| 115 |
+
document.getElementById('waiting-message').style.display = 'block';
|
| 116 |
+
});
|
static/style.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
background-color: #f4f4f4;
|
| 3 |
+
font-family: Arial, sans-serif;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
.container {
|
| 7 |
+
max-width: 800px;
|
| 8 |
+
margin: 0 auto;
|
| 9 |
+
background-color: #fff;
|
| 10 |
+
padding: 20px;
|
| 11 |
+
border-radius: 8px;
|
| 12 |
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
h2, p {
|
| 16 |
+
font-size: 18px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
input[type="radio"] {
|
| 20 |
+
margin-right: 10px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
input[type="submit"] {
|
| 24 |
+
display: block;
|
| 25 |
+
margin-top: 20px;
|
| 26 |
+
}
|
templates/client.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Quiz Client</title>
|
| 7 |
+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<h2 id="join-title">Join the Quiz</h2>
|
| 13 |
+
<input type="text" id="username" placeholder="Enter your name" class="form-control">
|
| 14 |
+
<button onclick="joinQuiz()" class="btn btn-primary mt-2">Join</button>
|
| 15 |
+
<div id="quiz-content" style="display: none;">
|
| 16 |
+
<h3>Logged in as: <span id="logged-user"></span></h3>
|
| 17 |
+
<h3 id="waiting-message" style="display: none;">Waiting for the Host...</h3>
|
| 18 |
+
<div id="question-section" class="mt-4">
|
| 19 |
+
<form id="quiz-form" onsubmit="submitForm(event)">
|
| 20 |
+
<p id="question-text"></p>
|
| 21 |
+
<div id="options"></div>
|
| 22 |
+
<input type="submit" value="Submit" class="btn btn-primary mt-2">
|
| 23 |
+
</form>
|
| 24 |
+
</div>
|
| 25 |
+
<div id="results" class="mt-4"></div>
|
| 26 |
+
</div>
|
| 27 |
+
<div id="final-results" style="display: none;" class="mt-4">
|
| 28 |
+
<h3>And the Winners are:</h3>
|
| 29 |
+
<table class="table mt-2">
|
| 30 |
+
<thead>
|
| 31 |
+
<tr>
|
| 32 |
+
<th>Participant</th>
|
| 33 |
+
<th>Score</th>
|
| 34 |
+
</tr>
|
| 35 |
+
</thead>
|
| 36 |
+
<tbody id="results-table">
|
| 37 |
+
</tbody>
|
| 38 |
+
</table>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
|
| 42 |
+
<script src="/static/script.js"></script>
|
| 43 |
+
</body>
|
| 44 |
+
</html>
|
templates/host.html
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Quiz Host</title>
|
| 7 |
+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<h2>Quiz Host</h2>
|
| 13 |
+
<p>Participants connected: <span id="participant-count">0</span></p>
|
| 14 |
+
<select id="exam-selector" class="form-control" onchange="selectExam()">
|
| 15 |
+
<option value="" disabled selected>Select an exam</option>
|
| 16 |
+
{% for exam in exams %}
|
| 17 |
+
<option value="{{ exam }}">{{ exam }}</option>
|
| 18 |
+
{% endfor %}
|
| 19 |
+
</select>
|
| 20 |
+
<label for="start-question" class="mt-3">Select starting question:</label>
|
| 21 |
+
<input type="range" id="start-question" min="1" max="10" value="1" class="form-range mt-2 mb-2" oninput="updateSliderValue(this.value)">
|
| 22 |
+
<input type="number" id="start-question-number" min="1" max="10" value="1" class="form-control" oninput="updateSliderValue(this.value)">
|
| 23 |
+
<p id="question-start-display" class="mt-2">Starting from question 1.</p>
|
| 24 |
+
<button onclick="loadQuiz()" class="btn btn-info mt-3">Load Quiz</button><br><br>
|
| 25 |
+
<button onclick="startQuiz()" class="btn btn-success mt-3">Start Quiz</button><br><br>
|
| 26 |
+
<button onclick="restartQuiz()" class="btn btn-warning mt-3">Restart</button><br><br>
|
| 27 |
+
<button onclick="checkAnswers()" class="btn btn-primary mt-2">Check Answers</button><br><br>
|
| 28 |
+
<button onclick="nextQuestion()" class="btn btn-secondary mt-2">Next Question</button><br><br>
|
| 29 |
+
<button onclick="endQuiz()" id="end-quiz" class="btn btn-danger mt-2" disabled>End Quiz</button>
|
| 30 |
+
<div id="question-section" class="mt-4">
|
| 31 |
+
<p id="question-text"></p>
|
| 32 |
+
<div id="options"></div>
|
| 33 |
+
</div>
|
| 34 |
+
<div id="results" class="mt-4"></div>
|
| 35 |
+
</div>
|
| 36 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
|
| 37 |
+
<script src="/static/script.js"></script>
|
| 38 |
+
</body>
|
| 39 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Quiz Application</title>
|
| 7 |
+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="container text-center">
|
| 11 |
+
<h1 class="mt-5">Real-Time Quiz Application</h1>
|
| 12 |
+
<button onclick="window.location.href='/client'" class="btn btn-primary mt-3">Join as Client</button>
|
| 13 |
+
<button onclick="window.location.href='/host'" class="btn btn-success mt-3">Join as Host</button>
|
| 14 |
+
</div>
|
| 15 |
+
</body>
|
| 16 |
+
</html>
|