ezquiz 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ezquiz/__init__.py +4 -0
- ezquiz/apigame.py +86 -0
- ezquiz/cligame.py +16 -0
- ezquiz/ezquiz.py +34 -0
- ezquiz/py.typed +0 -0
- ezquiz/templates/index.html +419 -0
- ezquiz-0.2.0.dist-info/METADATA +9 -0
- ezquiz-0.2.0.dist-info/RECORD +9 -0
- ezquiz-0.2.0.dist-info/WHEEL +4 -0
ezquiz/__init__.py
ADDED
ezquiz/apigame.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from random import choice
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
from fastapi import FastAPI, Request
|
|
7
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
8
|
+
from fastapi.templating import Jinja2Templates
|
|
9
|
+
|
|
10
|
+
from ezquiz.ezquiz import Q
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIGame:
|
|
14
|
+
def __init__(self, title: str, qs: dict[str, Q]) -> None:
|
|
15
|
+
self.title = title
|
|
16
|
+
self.qs = qs
|
|
17
|
+
self.score = 0
|
|
18
|
+
self.mistakes = 0
|
|
19
|
+
self.streak = 0
|
|
20
|
+
self.history = []
|
|
21
|
+
|
|
22
|
+
def start(
|
|
23
|
+
self,
|
|
24
|
+
mode: Literal["timed", "score", "mistakes", "total", "streak"] = "score",
|
|
25
|
+
param=10,
|
|
26
|
+
):
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
|
|
29
|
+
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
|
|
30
|
+
|
|
31
|
+
@app.get("/", response_class=HTMLResponse)
|
|
32
|
+
async def landing_page(request: Request):
|
|
33
|
+
return templates.TemplateResponse(
|
|
34
|
+
"index.html",
|
|
35
|
+
context={
|
|
36
|
+
"request": request,
|
|
37
|
+
"title": self.title,
|
|
38
|
+
"categories": list(self.qs.keys()),
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@app.post("/api/next", response_class=JSONResponse)
|
|
43
|
+
async def next_question(request: Request):
|
|
44
|
+
"""Get the next question in the session."""
|
|
45
|
+
# return a random question from selected categories
|
|
46
|
+
data = await request.json()
|
|
47
|
+
print(data)
|
|
48
|
+
categories = data.get("categories", [])
|
|
49
|
+
|
|
50
|
+
# Get all available questions from selected categories
|
|
51
|
+
cat = choice(categories)
|
|
52
|
+
|
|
53
|
+
q = self.qs[cat]
|
|
54
|
+
seed = q.get_seed()
|
|
55
|
+
prompt = q.ask(seed)
|
|
56
|
+
|
|
57
|
+
return JSONResponse(
|
|
58
|
+
{
|
|
59
|
+
"complete": False,
|
|
60
|
+
"question": {"category": cat, "seed": seed, "text": prompt},
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@app.post("/api/submit", response_class=JSONResponse)
|
|
65
|
+
async def submit_answer(request: Request):
|
|
66
|
+
data = await request.json()
|
|
67
|
+
print(data)
|
|
68
|
+
cat = data["category"]
|
|
69
|
+
seed = data["seed"]
|
|
70
|
+
submitted_ans = data["answer"]
|
|
71
|
+
|
|
72
|
+
q = self.qs[cat]
|
|
73
|
+
correct_ans = q.correct(seed)
|
|
74
|
+
correct = q.check(correct_ans, submitted_ans)
|
|
75
|
+
explain = q.explain(seed)
|
|
76
|
+
|
|
77
|
+
return JSONResponse(
|
|
78
|
+
{
|
|
79
|
+
"correct": correct,
|
|
80
|
+
"submitted_answer": submitted_ans,
|
|
81
|
+
"correct_answer": correct_ans,
|
|
82
|
+
"explanation": explain,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
uvicorn.run(app)
|
ezquiz/cligame.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CLIGame:
|
|
5
|
+
def __init__(self) -> None:
|
|
6
|
+
self.score = 0
|
|
7
|
+
self.mistakes = 0
|
|
8
|
+
self.streak = 0
|
|
9
|
+
self.history = []
|
|
10
|
+
|
|
11
|
+
def start(
|
|
12
|
+
self,
|
|
13
|
+
mode: Literal["timed", "score", "mistakes", "total", "streak"] = "score",
|
|
14
|
+
param=10,
|
|
15
|
+
):
|
|
16
|
+
pass
|
ezquiz/ezquiz.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Callable, Generic, Literal, TypedDict, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
Prompt = TypedDict("Prompt", {"type": Literal["text", "audio"], "value": str})
|
|
6
|
+
|
|
7
|
+
Explain = TypedDict("Explain", {"type": Literal["text", "text_diff"], "value": str})
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Q(Generic[T]):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
get_seed: Callable[[], T],
|
|
14
|
+
ask: Callable[[T], str],
|
|
15
|
+
correct: Callable[[T], str],
|
|
16
|
+
check: Callable[[T, str], bool] | None = None,
|
|
17
|
+
explain: Callable[[T], Explain] | None = None,
|
|
18
|
+
):
|
|
19
|
+
self.get_seed = get_seed
|
|
20
|
+
self.ask = ask
|
|
21
|
+
self.correct = correct
|
|
22
|
+
|
|
23
|
+
if check is None:
|
|
24
|
+
self.check = (
|
|
25
|
+
lambda correct_ans, submitted_ans: str(correct_ans) == submitted_ans
|
|
26
|
+
)
|
|
27
|
+
else:
|
|
28
|
+
self.check = check
|
|
29
|
+
|
|
30
|
+
if explain is None:
|
|
31
|
+
self.explain = lambda _: "{textdiff}"
|
|
32
|
+
|
|
33
|
+
else:
|
|
34
|
+
self.explain = explain
|
ezquiz/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,419 @@
|
|
|
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>{{ title }}</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-gray-50 min-h-screen">
|
|
10
|
+
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
|
11
|
+
<!-- Initial Setup View -->
|
|
12
|
+
<div id="setup-view" class="bg-white rounded-lg shadow-md p-6">
|
|
13
|
+
<h1 class="text-3xl font-bold text-gray-800 mb-6 text-center">{{ title }}</h1>
|
|
14
|
+
|
|
15
|
+
<div class="mb-6">
|
|
16
|
+
<h2 class="text-lg font-semibold text-gray-700 mb-3">Select Categories:</h2>
|
|
17
|
+
<div class="space-y-2" id="categories-container">
|
|
18
|
+
{% for value in categories %}
|
|
19
|
+
<label class="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer transition">
|
|
20
|
+
<input type="checkbox" name="categories" value="{{ value }}"
|
|
21
|
+
class="w-5 h-5 text-blue-600 rounded focus:ring-blue-500 category-checkbox">
|
|
22
|
+
<span class="text-gray-700">{{ value }}</span>
|
|
23
|
+
</label>
|
|
24
|
+
{% endfor %}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<button id="start-btn"
|
|
29
|
+
class="w-full bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg hover:bg-blue-700 transition duration-200">
|
|
30
|
+
Start Quiz
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Quiz View with Sidebar -->
|
|
35
|
+
<div id="quiz-view" class="hidden">
|
|
36
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
37
|
+
<!-- Sidebar with Categories -->
|
|
38
|
+
<div class="md:col-span-1">
|
|
39
|
+
<div class="bg-white rounded-lg shadow-md p-4 sticky top-4">
|
|
40
|
+
<h3 class="font-semibold text-gray-700 mb-3">Categories</h3>
|
|
41
|
+
<div class="space-y-2" id="sidebar-categories">
|
|
42
|
+
{% for value in categories %}
|
|
43
|
+
<label class="flex items-center space-x-2 cursor-pointer">
|
|
44
|
+
<input type="checkbox" name="sidebar-categories" value="{{ value }}"
|
|
45
|
+
class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 sidebar-category-checkbox">
|
|
46
|
+
<span class="text-sm text-gray-600">{{ value }}</span>
|
|
47
|
+
</label>
|
|
48
|
+
{% endfor %}
|
|
49
|
+
</div>
|
|
50
|
+
<button id="update-categories-btn"
|
|
51
|
+
class="w-full mt-4 bg-gray-600 text-white text-sm font-semibold py-2 px-4 rounded hover:bg-gray-700 transition duration-200">
|
|
52
|
+
Update
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Main Quiz Content -->
|
|
58
|
+
<div class="md:col-span-3">
|
|
59
|
+
<div class="bg-white rounded-lg shadow-md p-6">
|
|
60
|
+
<div class="flex justify-between items-center mb-6">
|
|
61
|
+
<span id="question-counter" class="text-sm text-gray-500">Question 1</span>
|
|
62
|
+
<span id="selected-categories-display" class="text-xs text-gray-400"></span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Question Container -->
|
|
66
|
+
<div id="question-container" class="mb-6">
|
|
67
|
+
<p id="question-text" class="text-xl text-gray-800 mb-4"></p>
|
|
68
|
+
<input type="text" id="answer-input"
|
|
69
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
70
|
+
placeholder="Your answer...">
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<button id="submit-btn"
|
|
74
|
+
class="w-full bg-green-600 text-white font-semibold py-3 px-6 rounded-lg hover:bg-green-700 transition duration-200">
|
|
75
|
+
Submit Answer (Enter)
|
|
76
|
+
</button>
|
|
77
|
+
|
|
78
|
+
<!-- Result Container (initially hidden) -->
|
|
79
|
+
<div id="result-container" class="mt-6 hidden">
|
|
80
|
+
<!-- Result content will be inserted here -->
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.diff {
|
|
90
|
+
font-family: "Courier New", monospace;
|
|
91
|
+
font-size: 1.15em;
|
|
92
|
+
white-space: pre;
|
|
93
|
+
margin-top: 1rem;
|
|
94
|
+
}
|
|
95
|
+
.diff-row {
|
|
96
|
+
margin-bottom: 2px;
|
|
97
|
+
line-height: 1.2;
|
|
98
|
+
}
|
|
99
|
+
.diff-green {
|
|
100
|
+
color: #2e7d32;
|
|
101
|
+
font-weight: bold;
|
|
102
|
+
}
|
|
103
|
+
.diff-red {
|
|
104
|
+
color: #c62828;
|
|
105
|
+
font-weight: bold;
|
|
106
|
+
}
|
|
107
|
+
.diff-grey {
|
|
108
|
+
color: #9e9e9e;
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
111
|
+
<script>
|
|
112
|
+
// Store selected categories globally
|
|
113
|
+
let selectedCategories = [];
|
|
114
|
+
let currentQuestion = null;
|
|
115
|
+
let questionNumber = 0;
|
|
116
|
+
let showingResult = false;
|
|
117
|
+
|
|
118
|
+
// Text diff functions
|
|
119
|
+
function escapeChar(c) {
|
|
120
|
+
return c === " " ? " " : c;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function alignStrings(a, b) {
|
|
124
|
+
// Handle null/undefined inputs
|
|
125
|
+
a = a || "";
|
|
126
|
+
b = b || "";
|
|
127
|
+
|
|
128
|
+
const dp = Array.from({ length: a.length + 1 }, () =>
|
|
129
|
+
Array(b.length + 1).fill(0),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
133
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
134
|
+
|
|
135
|
+
for (let i = 1; i <= a.length; i++) {
|
|
136
|
+
for (let j = 1; j <= b.length; j++) {
|
|
137
|
+
dp[i][j] =
|
|
138
|
+
a[i - 1] === b[j - 1]
|
|
139
|
+
? dp[i - 1][j - 1]
|
|
140
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let i = a.length,
|
|
145
|
+
j = b.length;
|
|
146
|
+
let aAlign = "",
|
|
147
|
+
bAlign = "";
|
|
148
|
+
|
|
149
|
+
while (i > 0 || j > 0) {
|
|
150
|
+
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
|
151
|
+
aAlign = a[i - 1] + aAlign;
|
|
152
|
+
bAlign = b[j - 1] + bAlign;
|
|
153
|
+
i--;
|
|
154
|
+
j--;
|
|
155
|
+
} else if (i > 0 && j > 0 && dp[i][j] === dp[i - 1][j - 1] + 1) {
|
|
156
|
+
aAlign = a[i - 1] + aAlign;
|
|
157
|
+
bAlign = b[j - 1] + bAlign;
|
|
158
|
+
i--;
|
|
159
|
+
j--;
|
|
160
|
+
} else if (i > 0 && dp[i][j] === dp[i - 1][j] + 1) {
|
|
161
|
+
aAlign = a[i - 1] + aAlign;
|
|
162
|
+
bAlign = " " + bAlign;
|
|
163
|
+
i--;
|
|
164
|
+
} else {
|
|
165
|
+
aAlign = " " + aAlign;
|
|
166
|
+
bAlign = b[j - 1] + bAlign;
|
|
167
|
+
j--;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { aAlign, bAlign };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderTextDiff(userAnswer, correctAnswer) {
|
|
175
|
+
// Handle null/undefined inputs
|
|
176
|
+
userAnswer = String(userAnswer || "");
|
|
177
|
+
correctAnswer = String(correctAnswer || "");
|
|
178
|
+
|
|
179
|
+
const { aAlign, bAlign } = alignStrings(userAnswer, correctAnswer);
|
|
180
|
+
|
|
181
|
+
let top = "";
|
|
182
|
+
let bottom = "";
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < aAlign.length; i++) {
|
|
185
|
+
const a = aAlign[i];
|
|
186
|
+
const b = bAlign[i];
|
|
187
|
+
|
|
188
|
+
// TOP ROW (user input)
|
|
189
|
+
if (a === " " && b !== " ") {
|
|
190
|
+
top += `<span class="diff-grey">-</span>`;
|
|
191
|
+
} else if (a === b) {
|
|
192
|
+
top += `<span class="diff-green">${escapeChar(a)}</span>`;
|
|
193
|
+
} else if (a !== " ") {
|
|
194
|
+
top += `<span class="diff-red">${escapeChar(a)}</span>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// BOTTOM ROW (reference)
|
|
198
|
+
if (b === " ") {
|
|
199
|
+
bottom += " ";
|
|
200
|
+
} else if (a === b) {
|
|
201
|
+
bottom += `<span class="diff-green">${escapeChar(b)}</span>`;
|
|
202
|
+
} else {
|
|
203
|
+
bottom += `<span class="diff-grey">${escapeChar(b)}</span>`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return `<div class="diff"><div class="diff-row">${top}</div><div class="diff-row">${bottom}</div></div>`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get DOM elements
|
|
211
|
+
const setupView = document.getElementById('setup-view');
|
|
212
|
+
const quizView = document.getElementById('quiz-view');
|
|
213
|
+
const startBtn = document.getElementById('start-btn');
|
|
214
|
+
const submitBtn = document.getElementById('submit-btn');
|
|
215
|
+
const updateCategoriesBtn = document.getElementById('update-categories-btn');
|
|
216
|
+
const questionText = document.getElementById('question-text');
|
|
217
|
+
const answerInput = document.getElementById('answer-input');
|
|
218
|
+
const resultContainer = document.getElementById('result-container');
|
|
219
|
+
const questionCounter = document.getElementById('question-counter');
|
|
220
|
+
const selectedCategoriesDisplay = document.getElementById('selected-categories-display');
|
|
221
|
+
const questionContainer = document.getElementById('question-container');
|
|
222
|
+
|
|
223
|
+
// Start button click handler
|
|
224
|
+
startBtn.addEventListener('click', async () => {
|
|
225
|
+
// Get selected categories
|
|
226
|
+
const checkboxes = document.querySelectorAll('.category-checkbox:checked');
|
|
227
|
+
selectedCategories = Array.from(checkboxes).map(cb => cb.value);
|
|
228
|
+
|
|
229
|
+
if (selectedCategories.length === 0) {
|
|
230
|
+
alert('Please select at least one category');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Sync sidebar checkboxes
|
|
235
|
+
syncSidebarCategories();
|
|
236
|
+
|
|
237
|
+
// Fetch first question
|
|
238
|
+
await fetchNextQuestion();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Update categories button click handler
|
|
242
|
+
updateCategoriesBtn.addEventListener('click', async () => {
|
|
243
|
+
const checkboxes = document.querySelectorAll('.sidebar-category-checkbox:checked');
|
|
244
|
+
selectedCategories = Array.from(checkboxes).map(cb => cb.value);
|
|
245
|
+
|
|
246
|
+
if (selectedCategories.length === 0) {
|
|
247
|
+
alert('Please select at least one category');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Fetch next question with updated categories
|
|
252
|
+
await fetchNextQuestion();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Sync sidebar checkboxes with selected categories
|
|
256
|
+
function syncSidebarCategories() {
|
|
257
|
+
document.querySelectorAll('.sidebar-category-checkbox').forEach(cb => {
|
|
258
|
+
cb.checked = selectedCategories.includes(cb.value);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fetch next question
|
|
263
|
+
async function fetchNextQuestion() {
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetch('/api/next', {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
'Content-Type': 'application/json'
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({ categories: selectedCategories })
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const data = await response.json();
|
|
274
|
+
|
|
275
|
+
if (data.complete) {
|
|
276
|
+
// Quiz complete
|
|
277
|
+
alert('Quiz complete! Great job!');
|
|
278
|
+
resetQuiz();
|
|
279
|
+
} else {
|
|
280
|
+
currentQuestion = data.question;
|
|
281
|
+
questionNumber++;
|
|
282
|
+
showQuestion();
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Error fetching question:', error);
|
|
286
|
+
alert('Failed to load question. Please try again.');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Submit button click handler
|
|
291
|
+
submitBtn.addEventListener('click', async () => {
|
|
292
|
+
if (showingResult) {
|
|
293
|
+
// If showing result, Enter goes to next question
|
|
294
|
+
await fetchNextQuestion();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const answer = answerInput.value.trim();
|
|
299
|
+
|
|
300
|
+
if (!answer) {
|
|
301
|
+
alert('Please enter an answer');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch('/api/submit', {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: {
|
|
309
|
+
'Content-Type': 'application/json'
|
|
310
|
+
},
|
|
311
|
+
body: JSON.stringify({
|
|
312
|
+
category: currentQuestion.category,
|
|
313
|
+
seed: currentQuestion.seed,
|
|
314
|
+
answer: answer,
|
|
315
|
+
})
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const data = await response.json();
|
|
319
|
+
showResult(data);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error('Error submitting answer:', error);
|
|
322
|
+
alert('Failed to submit answer. Please try again.');
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Show question view
|
|
327
|
+
function showQuestion() {
|
|
328
|
+
setupView.classList.add('hidden');
|
|
329
|
+
quizView.classList.remove('hidden');
|
|
330
|
+
|
|
331
|
+
// Reset UI
|
|
332
|
+
resultContainer.classList.add('hidden');
|
|
333
|
+
resultContainer.innerHTML = '';
|
|
334
|
+
questionContainer.classList.remove('hidden');
|
|
335
|
+
submitBtn.textContent = 'Submit Answer (Enter)';
|
|
336
|
+
showingResult = false;
|
|
337
|
+
|
|
338
|
+
questionText.textContent = currentQuestion.text;
|
|
339
|
+
questionCounter.textContent = `Question ${questionNumber}`;
|
|
340
|
+
selectedCategoriesDisplay.textContent = selectedCategories.join(', ');
|
|
341
|
+
answerInput.value = '';
|
|
342
|
+
answerInput.focus();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Show result view
|
|
346
|
+
function showResult(data) {
|
|
347
|
+
showingResult = true;
|
|
348
|
+
questionContainer.classList.add('hidden');
|
|
349
|
+
resultContainer.classList.remove('hidden');
|
|
350
|
+
submitBtn.textContent = 'Next Question (Enter)';
|
|
351
|
+
|
|
352
|
+
const isCorrect = data.correct;
|
|
353
|
+
const bgColor = isCorrect ? 'bg-green-100' : 'bg-red-100';
|
|
354
|
+
const textColor = isCorrect ? 'text-green-800' : 'text-red-800';
|
|
355
|
+
const icon = isCorrect ? '✓' : '✗';
|
|
356
|
+
const message = isCorrect ? 'Correct!' : 'Incorrect';
|
|
357
|
+
|
|
358
|
+
// Check if explanation is textdiff placeholder
|
|
359
|
+
const isTextDiff = data.explanation === "{textdiff}";
|
|
360
|
+
|
|
361
|
+
let explanationHtml = '';
|
|
362
|
+
if (!isCorrect && isTextDiff) {
|
|
363
|
+
explanationHtml = `
|
|
364
|
+
<div class="bg-gray-50 border-l-4 border-gray-400 p-4">
|
|
365
|
+
${renderTextDiff(data.submitted_answer, data.correct_answer)}
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
} else if (!isCorrect && data.explanation) {
|
|
369
|
+
explanationHtml = `<div class="bg-blue-50 border-l-4 border-blue-500 p-4"><p class="text-gray-700"><strong>Explanation:</strong> ${data.explanation}</p></div>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
resultContainer.innerHTML = `
|
|
373
|
+
<div class="${bgColor} border-l-4 ${isCorrect ? 'border-green-500' : 'border-red-500'} p-4 mb-4">
|
|
374
|
+
<div class="flex items-center">
|
|
375
|
+
<span class="text-2xl mr-3">${icon}</span>
|
|
376
|
+
<div>
|
|
377
|
+
<p class="font-bold ${textColor} text-lg">${message}</p>
|
|
378
|
+
<p class="text-gray-600 mt-1">Your answer: <span class="font-semibold">${data.submitted_answer}</span></p>
|
|
379
|
+
${!isCorrect && !isTextDiff ? `<p class="text-gray-600 mt-1">Correct answer: <span class="font-semibold">${data.correct_answer}</span></p>` : ''}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
${explanationHtml}
|
|
384
|
+
<div class="mt-4 text-sm text-gray-500 text-center">Press Enter to continue</div>
|
|
385
|
+
`;
|
|
386
|
+
|
|
387
|
+
// Focus on document to capture Enter key
|
|
388
|
+
document.activeElement.blur();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Reset quiz to initial state
|
|
392
|
+
function resetQuiz() {
|
|
393
|
+
quizView.classList.add('hidden');
|
|
394
|
+
setupView.classList.remove('hidden');
|
|
395
|
+
|
|
396
|
+
// Uncheck all checkboxes
|
|
397
|
+
document.querySelectorAll('.category-checkbox, .sidebar-category-checkbox').forEach(cb => {
|
|
398
|
+
cb.checked = false;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
selectedCategories = [];
|
|
402
|
+
currentQuestion = null;
|
|
403
|
+
questionNumber = 0;
|
|
404
|
+
showingResult = false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Allow Enter key to submit answer or go to next question
|
|
408
|
+
document.addEventListener('keypress', (e) => {
|
|
409
|
+
if (e.key === 'Enter' && !setupView.classList.contains('hidden')) {
|
|
410
|
+
// Don't handle Enter on setup view
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (e.key === 'Enter' && !quizView.classList.contains('hidden')) {
|
|
414
|
+
submitBtn.click();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
</script>
|
|
418
|
+
</body>
|
|
419
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ezquiz
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: Alejandro Rodriguez <alejandro.rodriguez@alexanderthamm.com>
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
|
+
Requires-Dist: fastapi>=0.128.0
|
|
8
|
+
Requires-Dist: jinja2>=3.1.6
|
|
9
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ezquiz/__init__.py,sha256=_Ycs0PzsU0K53tGDxZMRP1kQ-TvIrF2_hTYNniOymxo,109
|
|
2
|
+
ezquiz/apigame.py,sha256=0AH_Ll4Mxg8FW9UZVei--tXp6rTI9wuEQETnFFX2mR4,2618
|
|
3
|
+
ezquiz/cligame.py,sha256=h_bs4Cm-44ibzpacIycEGaprnFWYKsyTJuyoeaGEYRk,325
|
|
4
|
+
ezquiz/ezquiz.py,sha256=ix00WYO6Zy2EFaS1xHvW6dyzGIz7OdTRac7MCcHuGI4,941
|
|
5
|
+
ezquiz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
ezquiz/templates/index.html,sha256=2_XCV4ic4_Dfcqsyg0MxvilCAQ06lvKxjOBa9YzLkM0,17262
|
|
7
|
+
ezquiz-0.2.0.dist-info/METADATA,sha256=vnLPjO6_NafAFZOWnYyTruX3fS-G-4j6mJ3XWocuQbs,276
|
|
8
|
+
ezquiz-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
ezquiz-0.2.0.dist-info/RECORD,,
|