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>
|