ezquiz 0.2.1__py3-none-any.whl → 0.3.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/apigame.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from pathlib import Path
2
2
  from random import choice
3
- from typing import Literal
4
3
 
5
4
  import uvicorn
6
5
  from fastapi import FastAPI, Request
7
6
  from fastapi.responses import HTMLResponse, JSONResponse
7
+ from fastapi.staticfiles import StaticFiles
8
8
  from fastapi.templating import Jinja2Templates
9
9
 
10
10
  from ezquiz.ezquiz import Q
@@ -21,9 +21,17 @@ class APIGame:
21
21
 
22
22
  def start(
23
23
  self,
24
+ *,
25
+ host: str,
26
+ port: int,
24
27
  **fastapi_kw,
25
28
  ):
26
29
  app = FastAPI(**fastapi_kw)
30
+ app.mount(
31
+ "/static",
32
+ StaticFiles(directory=Path(__file__).parent / "static"),
33
+ name="static",
34
+ )
27
35
 
28
36
  templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
29
37
 
@@ -56,7 +64,14 @@ class APIGame:
56
64
  return JSONResponse(
57
65
  {
58
66
  "complete": False,
59
- "question": {"category": cat, "seed": seed, "text": prompt},
67
+ "question": {
68
+ "category": cat,
69
+ "seed": seed,
70
+ "text": prompt["text"],
71
+ "type": prompt.get("type", "simple"),
72
+ "context": prompt.get("context", ""),
73
+ "hints": prompt.get("hints", []),
74
+ },
60
75
  }
61
76
  )
62
77
 
@@ -82,4 +97,4 @@ class APIGame:
82
97
  }
83
98
  )
84
99
 
85
- uvicorn.run(app)
100
+ uvicorn.run(app, host=host, port=port)
ezquiz/ezquiz.py CHANGED
@@ -1,3 +1,4 @@
1
+ from random import choice
1
2
  from typing import Callable, Generic, Literal, TypedDict, TypeVar
2
3
 
3
4
  T = TypeVar("T")
@@ -11,7 +12,7 @@ class Q(Generic[T]):
11
12
  def __init__(
12
13
  self,
13
14
  get_seed: Callable[[], T],
14
- ask: Callable[[T], str],
15
+ ask: Callable[[T], dict],
15
16
  correct: Callable[[T], str],
16
17
  check: Callable[[T, str], bool] | None = None,
17
18
  explain: Callable[[T], Explain] | None = None,
@@ -32,3 +33,17 @@ class Q(Generic[T]):
32
33
 
33
34
  else:
34
35
  self.explain = explain
36
+
37
+ @classmethod
38
+ def from_dict(cls, dct: dict, question_type: str = "simple", **kwargs):
39
+ return cls(
40
+ get_seed=lambda: choice(list(dct.keys())),
41
+ ask=lambda seed: {
42
+ "text": str(seed),
43
+ "type": question_type,
44
+ "context": "",
45
+ "hints": [],
46
+ },
47
+ correct=lambda seed: dct[seed],
48
+ **kwargs,
49
+ )
@@ -0,0 +1,51 @@
1
+ /**
2
+ * API communication module
3
+ */
4
+
5
+ /**
6
+ * Fetch the next question from the API
7
+ * @param {string[]} categories - Selected category names
8
+ * @returns {Promise<Object>} Question data or completion status
9
+ */
10
+ export async function fetchNextQuestion(categories) {
11
+ const response = await fetch('api/next', {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json'
15
+ },
16
+ body: JSON.stringify({ categories })
17
+ });
18
+
19
+ if (!response.ok) {
20
+ throw new Error(`HTTP error! status: ${response.status}`);
21
+ }
22
+
23
+ return response.json();
24
+ }
25
+
26
+ /**
27
+ * Submit an answer to the API
28
+ * @param {string} category - Question category
29
+ * @param {*} seed - Question seed
30
+ * @param {string} answer - User's answer
31
+ * @returns {Promise<Object>} Result with correctness and explanation
32
+ */
33
+ export async function submitAnswer(category, seed, answer) {
34
+ const response = await fetch('api/submit', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json'
38
+ },
39
+ body: JSON.stringify({
40
+ category,
41
+ seed,
42
+ answer: answer.trim()
43
+ })
44
+ });
45
+
46
+ if (!response.ok) {
47
+ throw new Error(`HTTP error! status: ${response.status}`);
48
+ }
49
+
50
+ return response.json();
51
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Text diff visualization module
3
+ * Implements Levenshtein distance algorithm for string alignment
4
+ */
5
+
6
+ /**
7
+ * Escape special characters for HTML display
8
+ * @param {string} c - Character to escape
9
+ * @returns {string} Escaped character
10
+ */
11
+ function escapeChar(c) {
12
+ return c === " " ? "&nbsp;" : c;
13
+ }
14
+
15
+ /**
16
+ * Align two strings using dynamic programming (Levenshtein distance)
17
+ * @param {string} a - First string
18
+ * @param {string} b - Second string
19
+ * @returns {{aAlign: string, bAlign: string}} Aligned strings
20
+ */
21
+ function alignStrings(a, b) {
22
+ a = a || "";
23
+ b = b || "";
24
+
25
+ const dp = Array.from({ length: a.length + 1 }, () =>
26
+ Array(b.length + 1).fill(0),
27
+ );
28
+
29
+ for (let i = 0; i <= a.length; i++) dp[i][0] = i;
30
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
31
+
32
+ for (let i = 1; i <= a.length; i++) {
33
+ for (let j = 1; j <= b.length; j++) {
34
+ dp[i][j] =
35
+ a[i - 1] === b[j - 1]
36
+ ? dp[i - 1][j - 1]
37
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
38
+ }
39
+ }
40
+
41
+ let i = a.length,
42
+ j = b.length;
43
+ let aAlign = "",
44
+ bAlign = "";
45
+
46
+ while (i > 0 || j > 0) {
47
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
48
+ aAlign = a[i - 1] + aAlign;
49
+ bAlign = b[j - 1] + bAlign;
50
+ i--;
51
+ j--;
52
+ } else if (i > 0 && j > 0 && dp[i][j] === dp[i - 1][j - 1] + 1) {
53
+ aAlign = a[i - 1] + aAlign;
54
+ bAlign = b[j - 1] + bAlign;
55
+ i--;
56
+ j--;
57
+ } else if (i > 0 && dp[i][j] === dp[i - 1][j] + 1) {
58
+ aAlign = a[i - 1] + aAlign;
59
+ bAlign = " " + bAlign;
60
+ i--;
61
+ } else {
62
+ aAlign = " " + aAlign;
63
+ bAlign = b[j - 1] + bAlign;
64
+ j--;
65
+ }
66
+ }
67
+
68
+ return { aAlign, bAlign };
69
+ }
70
+
71
+ /**
72
+ * Render a visual diff between two strings
73
+ * @param {string} userAnswer - User's submitted answer
74
+ * @param {string} correctAnswer - Correct answer
75
+ * @returns {string} HTML string with visual diff
76
+ */
77
+ export function renderTextDiff(userAnswer, correctAnswer) {
78
+ userAnswer = String(userAnswer || "");
79
+ correctAnswer = String(correctAnswer || "");
80
+
81
+ const { aAlign, bAlign } = alignStrings(userAnswer, correctAnswer);
82
+
83
+ let top = "";
84
+ let bottom = "";
85
+
86
+ for (let i = 0; i < aAlign.length; i++) {
87
+ const a = aAlign[i];
88
+ const b = bAlign[i];
89
+
90
+ // TOP ROW (user input)
91
+ if (a === " " && b !== " ") {
92
+ top += `<span class="diff-grey">-</span>`;
93
+ } else if (a === b) {
94
+ top += `<span class="diff-green">${escapeChar(a)}</span>`;
95
+ } else if (a !== " ") {
96
+ top += `<span class="diff-red">${escapeChar(a)}</span>`;
97
+ }
98
+
99
+ // BOTTOM ROW (reference)
100
+ if (b === " ") {
101
+ bottom += " ";
102
+ } else if (a === b) {
103
+ bottom += `<span class="diff-green">${escapeChar(b)}</span>`;
104
+ } else {
105
+ bottom += `<span class="diff-grey">${escapeChar(b)}</span>`;
106
+ }
107
+ }
108
+
109
+ return `<div class="diff"><div class="diff-row">${top}</div><div class="diff-row">${bottom}</div></div>`;
110
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Main entry point
3
+ * Initializes all view modules
4
+ */
5
+
6
+ import { initSetup } from './views/setup.js';
7
+ import { initQuiz } from './views/quiz.js';
8
+ import { initSidebar } from './views/sidebar.js';
9
+
10
+ document.addEventListener('DOMContentLoaded', () => {
11
+ initSetup();
12
+ initSidebar();
13
+ initQuiz();
14
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * State management module
3
+ * Encapsulates all application state
4
+ */
5
+
6
+ class State {
7
+ constructor() {
8
+ this.selectedCategories = [];
9
+ this.currentQuestion = null;
10
+ this.questionNumber = 0;
11
+ this.showingResult = false;
12
+ }
13
+
14
+ selectCategories(categories) {
15
+ this.selectedCategories = [...categories];
16
+ }
17
+
18
+ setQuestion(question) {
19
+ this.currentQuestion = question;
20
+ this.questionNumber++;
21
+ this.showingResult = false;
22
+ }
23
+
24
+ markShowingResult() {
25
+ this.showingResult = true;
26
+ }
27
+
28
+ reset() {
29
+ this.selectedCategories = [];
30
+ this.currentQuestion = null;
31
+ this.questionNumber = 0;
32
+ this.showingResult = false;
33
+ }
34
+ }
35
+
36
+ export const state = new State();
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Quiz view module
3
+ * Handles the question display and answer submission
4
+ */
5
+
6
+ import { state } from '../state.js';
7
+ import { submitAnswer, fetchNextQuestion } from '../api.js';
8
+ import { showResult } from './results.js';
9
+
10
+ const setupView = document.getElementById('setup-view');
11
+ const quizView = document.getElementById('quiz-view');
12
+ const questionText = document.getElementById('question-text');
13
+ const answerInput = document.getElementById('answer-input');
14
+ const questionCounter = document.getElementById('question-counter');
15
+ const selectedCategoriesDisplay = document.getElementById('selected-categories-display');
16
+ const submitBtn = document.getElementById('submit-btn');
17
+ const questionContainer = document.getElementById('question-container');
18
+ const resultContainer = document.getElementById('result-container');
19
+ const contextContainer = document.getElementById('context-container');
20
+ const questionContext = document.getElementById('question-context');
21
+
22
+ /**
23
+ * Display the quiz interface with current question
24
+ * Supports both "simple" and "fill" question types
25
+ */
26
+ export function showQuiz() {
27
+ setupView.classList.add('hidden');
28
+ quizView.classList.remove('hidden');
29
+
30
+ // Reset UI for new question
31
+ resultContainer.classList.add('hidden');
32
+ resultContainer.innerHTML = '';
33
+ questionContainer.classList.remove('hidden');
34
+ submitBtn.textContent = 'Submit Answer (Enter)';
35
+
36
+ questionCounter.textContent = `Question ${state.questionNumber}`;
37
+ selectedCategoriesDisplay.textContent = state.selectedCategories.join(', ');
38
+
39
+ const question = state.currentQuestion;
40
+
41
+ // Display context if present
42
+ if (question.context) {
43
+ questionContext.textContent = question.context;
44
+ contextContainer.classList.remove('hidden');
45
+ } else {
46
+ contextContainer.classList.add('hidden');
47
+ questionContext.textContent = '';
48
+ }
49
+
50
+ if (question.type === 'fill') {
51
+ renderFillQuestion(question);
52
+ } else {
53
+ renderSimpleQuestion(question);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Render a simple question with text and input field below
59
+ * @param {Object} question - Question object with text
60
+ */
61
+ function renderSimpleQuestion(question) {
62
+ questionContainer.innerHTML = `
63
+ <p id="question-text" class="text-xl text-gray-800 mb-4">${escapeHtml(question.text)}</p>
64
+ <input type="text" id="answer-input"
65
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
66
+ placeholder="Your answer...">
67
+ `;
68
+
69
+ const input = document.getElementById('answer-input');
70
+ if (input) {
71
+ input.focus();
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Render a fill-in-the-blank question with inline input
77
+ * @param {Object} question - Question object with text containing [...]
78
+ */
79
+ function renderFillQuestion(question) {
80
+ const parts = question.text.split('[...]');
81
+
82
+ if (parts.length !== 2) {
83
+ // Fallback to simple if [...] not found or appears multiple times
84
+ renderSimpleQuestion(question);
85
+ return;
86
+ }
87
+
88
+ questionContainer.innerHTML = `
89
+ <div class="fill-container text-xl text-gray-800">
90
+ <span>${escapeHtml(parts[0])}</span>
91
+ <input type="text" id="answer-input"
92
+ class="inline-input"
93
+ placeholder="">
94
+ <span>${escapeHtml(parts[1])}</span>
95
+ </div>
96
+ `;
97
+
98
+ const input = document.getElementById('answer-input');
99
+ if (input) {
100
+ input.focus();
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Escape HTML special characters
106
+ * @param {string} text - Text to escape
107
+ * @returns {string} Escaped text
108
+ */
109
+ function escapeHtml(text) {
110
+ const div = document.createElement('div');
111
+ div.textContent = text;
112
+ return div.innerHTML;
113
+ }
114
+
115
+ /**
116
+ * Get the current answer from the input field
117
+ * @returns {string} The answer value
118
+ */
119
+ function getAnswer() {
120
+ const input = document.getElementById('answer-input');
121
+ return input ? input.value.trim() : '';
122
+ }
123
+
124
+ /**
125
+ * Handle answer submission
126
+ */
127
+ async function handleSubmit() {
128
+ if (state.showingResult) {
129
+ // Get next question
130
+ try {
131
+ const data = await fetchNextQuestion(state.selectedCategories);
132
+
133
+ if (data.complete) {
134
+ alert('Quiz complete! Great job!');
135
+ import('./setup.js').then(({ resetSetup }) => resetSetup());
136
+ return;
137
+ }
138
+
139
+ state.setQuestion(data.question);
140
+ showQuiz();
141
+ } catch (error) {
142
+ console.error('Error fetching question:', error);
143
+ alert('Failed to load next question. Please try again.');
144
+ }
145
+ return;
146
+ }
147
+
148
+ const answer = getAnswer();
149
+
150
+ if (!answer) {
151
+ alert('Please enter an answer');
152
+ return;
153
+ }
154
+
155
+ try {
156
+ const data = await submitAnswer(
157
+ state.currentQuestion.category,
158
+ state.currentQuestion.seed,
159
+ answer
160
+ );
161
+
162
+ state.markShowingResult();
163
+ showResult(data);
164
+ } catch (error) {
165
+ console.error('Error submitting answer:', error);
166
+ alert('Failed to submit answer. Please try again.');
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Initialize the quiz view
172
+ */
173
+ export function initQuiz() {
174
+ submitBtn.addEventListener('click', handleSubmit);
175
+
176
+ // Enter key handler for quiz view
177
+ document.addEventListener('keypress', (e) => {
178
+ if (e.key === 'Enter' && !setupView.classList.contains('hidden')) {
179
+ return;
180
+ }
181
+ if (e.key === 'Enter' && !quizView.classList.contains('hidden')) {
182
+ handleSubmit();
183
+ }
184
+ });
185
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Results view module
3
+ * Handles display of answer feedback and explanations
4
+ */
5
+
6
+ import { renderTextDiff } from '../diff.js';
7
+ import { state } from '../state.js';
8
+
9
+ const submitBtn = document.getElementById('submit-btn');
10
+ const questionContainer = document.getElementById('question-container');
11
+ const resultContainer = document.getElementById('result-container');
12
+
13
+ /**
14
+ * Display the result of an answer submission
15
+ * @param {Object} data - Result data from API
16
+ * @param {boolean} data.correct - Whether answer was correct
17
+ * @param {string} data.submitted_answer - User's answer
18
+ * @param {string} data.correct_answer - Correct answer
19
+ * @param {string} data.explanation - Explanation or "{textdiff}"
20
+ */
21
+ export function showResult(data) {
22
+ questionContainer.classList.add('hidden');
23
+ resultContainer.classList.remove('hidden');
24
+ submitBtn.textContent = 'Next Question (Enter)';
25
+
26
+ const isCorrect = data.correct;
27
+ const bgColor = isCorrect ? 'bg-green-100' : 'bg-red-100';
28
+ const textColor = isCorrect ? 'text-green-800' : 'text-red-800';
29
+ const icon = isCorrect ? '✓' : '✗';
30
+ const message = isCorrect ? 'Correct!' : 'Incorrect';
31
+
32
+ // Check if explanation is textdiff placeholder
33
+ const isTextDiff = data.explanation === "{textdiff}";
34
+
35
+ let explanationHtml = '';
36
+ if (!isCorrect && isTextDiff) {
37
+ explanationHtml = `
38
+ <div class="bg-gray-50 border-l-4 border-gray-400 p-4">
39
+ ${renderTextDiff(data.submitted_answer, data.correct_answer)}
40
+ </div>
41
+ `;
42
+ } else if (!isCorrect && data.explanation) {
43
+ 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>`;
44
+ }
45
+
46
+ resultContainer.innerHTML = `
47
+ <div class="${bgColor} border-l-4 ${isCorrect ? 'border-green-500' : 'border-red-500'} p-4 mb-4">
48
+ <div class="flex items-center">
49
+ <span class="text-2xl mr-3">${icon}</span>
50
+ <div>
51
+ <p class="font-bold ${textColor} text-lg">${message}</p>
52
+ <p class="text-gray-600 mt-1">Your answer: <span class="font-semibold">${data.submitted_answer}</span></p>
53
+ ${!isCorrect && !isTextDiff ? `<p class="text-gray-600 mt-1">Correct answer: <span class="font-semibold">${data.correct_answer}</span></p>` : ''}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ ${explanationHtml}
58
+ <div class="mt-4 text-sm text-gray-500 text-center">Press Enter to continue</div>
59
+ `;
60
+
61
+ // Blur focus to prevent double submission
62
+ if (document.activeElement) {
63
+ document.activeElement.blur();
64
+ }
65
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Setup view module
3
+ * Handles category selection and quiz initialization
4
+ */
5
+
6
+ import { state } from '../state.js';
7
+ import { showQuiz } from './quiz.js';
8
+ import { fetchNextQuestion } from '../api.js';
9
+
10
+ const setupView = document.getElementById('setup-view');
11
+ const startBtn = document.getElementById('start-btn');
12
+
13
+ /**
14
+ * Get selected categories from checkboxes
15
+ * @param {string} selector - Checkbox selector
16
+ * @returns {string[]} Selected category values
17
+ */
18
+ function getSelectedCategories(selector) {
19
+ const checkboxes = document.querySelectorAll(selector);
20
+ return Array.from(checkboxes).map(cb => cb.value);
21
+ }
22
+
23
+ /**
24
+ * Sync sidebar checkboxes with current state
25
+ */
26
+ function syncSidebarCategories() {
27
+ document.querySelectorAll('.sidebar-category-checkbox').forEach(cb => {
28
+ cb.checked = state.selectedCategories.includes(cb.value);
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Load and display the first question
34
+ */
35
+ async function loadFirstQuestion() {
36
+ try {
37
+ const data = await fetchNextQuestion(state.selectedCategories);
38
+
39
+ if (data.complete) {
40
+ alert('Quiz complete! Great job!');
41
+ resetSetup();
42
+ return;
43
+ }
44
+
45
+ state.setQuestion(data.question);
46
+ showQuiz();
47
+ } catch (error) {
48
+ console.error('Error fetching question:', error);
49
+ alert('Failed to load question. Please try again.');
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Reset the setup view
55
+ */
56
+ function resetSetup() {
57
+ setupView.classList.remove('hidden');
58
+ document.getElementById('quiz-view').classList.add('hidden');
59
+
60
+ document.querySelectorAll('.category-checkbox, .sidebar-category-checkbox').forEach(cb => {
61
+ cb.checked = false;
62
+ });
63
+
64
+ state.reset();
65
+ }
66
+
67
+ /**
68
+ * Initialize the setup view
69
+ */
70
+ export function initSetup() {
71
+ startBtn.addEventListener('click', async () => {
72
+ const categories = getSelectedCategories('.category-checkbox:checked');
73
+
74
+ if (categories.length === 0) {
75
+ alert('Please select at least one category');
76
+ return;
77
+ }
78
+
79
+ state.selectCategories(categories);
80
+ syncSidebarCategories();
81
+ await loadFirstQuestion();
82
+ });
83
+ }
84
+
85
+ export { resetSetup, getSelectedCategories };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Sidebar view module
3
+ * Handles category updates during quiz
4
+ */
5
+
6
+ import { state } from '../state.js';
7
+ import { fetchNextQuestion } from '../api.js';
8
+ import { showQuiz } from './quiz.js';
9
+
10
+ const updateCategoriesBtn = document.getElementById('update-categories-btn');
11
+
12
+ /**
13
+ * Get selected categories from sidebar checkboxes
14
+ * @returns {string[]} Selected category values
15
+ */
16
+ function getSidebarCategories() {
17
+ const checkboxes = document.querySelectorAll('.sidebar-category-checkbox:checked');
18
+ return Array.from(checkboxes).map(cb => cb.value);
19
+ }
20
+
21
+ /**
22
+ * Update categories and load next question
23
+ */
24
+ async function handleUpdateCategories() {
25
+ const categories = getSidebarCategories();
26
+
27
+ if (categories.length === 0) {
28
+ alert('Please select at least one category');
29
+ return;
30
+ }
31
+
32
+ state.selectCategories(categories);
33
+
34
+ try {
35
+ const data = await fetchNextQuestion(state.selectedCategories);
36
+
37
+ if (data.complete) {
38
+ alert('Quiz complete! Great job!');
39
+ const { resetSetup } = await import('./setup.js');
40
+ resetSetup();
41
+ return;
42
+ }
43
+
44
+ state.setQuestion(data.question);
45
+ showQuiz();
46
+ } catch (error) {
47
+ console.error('Error fetching question:', error);
48
+ alert('Failed to update categories. Please try again.');
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Initialize the sidebar
54
+ */
55
+ export function initSidebar() {
56
+ updateCategoriesBtn.addEventListener('click', handleUpdateCategories);
57
+ }
@@ -0,0 +1,57 @@
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
+ <style>
9
+ .diff {
10
+ font-family: "Courier New", monospace;
11
+ font-size: 1.15em;
12
+ white-space: pre;
13
+ margin-top: 1rem;
14
+ }
15
+ .diff-row {
16
+ margin-bottom: 2px;
17
+ line-height: 1.2;
18
+ }
19
+ .diff-green {
20
+ color: #2e7d32;
21
+ font-weight: bold;
22
+ }
23
+ .diff-red {
24
+ color: #c62828;
25
+ font-weight: bold;
26
+ }
27
+ .diff-grey {
28
+ color: #9e9e9e;
29
+ }
30
+ .inline-input {
31
+ display: inline-block;
32
+ max-width: 16ch;
33
+ border: none;
34
+ border-bottom: 2px solid #374151;
35
+ background: transparent;
36
+ padding: 0 4px;
37
+ font-size: inherit;
38
+ font-family: inherit;
39
+ outline: none;
40
+ color: inherit;
41
+ text-align: center;
42
+ }
43
+ .inline-input:focus {
44
+ border-bottom-color: #2563eb;
45
+ }
46
+ .fill-container {
47
+ line-height: 2;
48
+ }
49
+ </style>
50
+ </head>
51
+ <body class="bg-gray-50 min-h-screen">
52
+ <div class="container mx-auto px-4 py-8 max-w-4xl">
53
+ {% block content %}{% endblock %}
54
+ </div>
55
+ <script type="module" src="static/js/main.js"></script>
56
+ </body>
57
+ </html>
@@ -0,0 +1,39 @@
1
+ <!-- Quiz View with Sidebar -->
2
+ <div id="quiz-view" class="hidden">
3
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
4
+ {% include "components/sidebar.html" %}
5
+
6
+ <!-- Main Quiz Content -->
7
+ <div class="md:col-span-3">
8
+ <div class="bg-white rounded-lg shadow-md p-6">
9
+ <div class="flex justify-between items-center mb-6">
10
+ <span id="question-counter" class="text-sm text-gray-500">Question 1</span>
11
+ <span id="selected-categories-display" class="text-xs text-gray-400"></span>
12
+ </div>
13
+
14
+ <!-- Context Display (hidden if empty) -->
15
+ <div id="context-container" class="mb-4 hidden">
16
+ <p id="question-context" class="text-sm text-gray-500 italic whitespace-pre-wrap"></p>
17
+ </div>
18
+
19
+ <!-- Question Container -->
20
+ <div id="question-container" class="mb-6">
21
+ <p id="question-text" class="text-xl text-gray-800 mb-4"></p>
22
+ <input type="text" id="answer-input"
23
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
24
+ placeholder="Your answer...">
25
+ </div>
26
+
27
+ <button id="submit-btn"
28
+ class="w-full bg-green-600 text-white font-semibold py-3 px-6 rounded-lg hover:bg-green-700 transition duration-200">
29
+ Submit Answer (Enter)
30
+ </button>
31
+
32
+ <!-- Result Container (initially hidden) -->
33
+ <div id="result-container" class="mt-6 hidden">
34
+ <!-- Result content will be inserted here -->
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
@@ -0,0 +1,22 @@
1
+ <!-- Initial Setup View -->
2
+ <div id="setup-view" class="bg-white rounded-lg shadow-md p-6">
3
+ <h1 class="text-3xl font-bold text-gray-800 mb-6 text-center">{{ title }}</h1>
4
+
5
+ <div class="mb-6">
6
+ <h2 class="text-lg font-semibold text-gray-700 mb-3">Select Categories:</h2>
7
+ <div class="space-y-2" id="categories-container">
8
+ {% for value in categories %}
9
+ <label class="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer transition">
10
+ <input type="checkbox" name="categories" value="{{ value }}"
11
+ class="w-5 h-5 text-blue-600 rounded focus:ring-blue-500 category-checkbox">
12
+ <span class="text-gray-700">{{ value }}</span>
13
+ </label>
14
+ {% endfor %}
15
+ </div>
16
+ </div>
17
+
18
+ <button id="start-btn"
19
+ class="w-full bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg hover:bg-blue-700 transition duration-200">
20
+ Start Quiz
21
+ </button>
22
+ </div>
@@ -0,0 +1,19 @@
1
+ <!-- Sidebar with Categories -->
2
+ <div class="md:col-span-1">
3
+ <div class="bg-white rounded-lg shadow-md p-4 sticky top-4">
4
+ <h3 class="font-semibold text-gray-700 mb-3">Categories</h3>
5
+ <div class="space-y-2" id="sidebar-categories">
6
+ {% for value in categories %}
7
+ <label class="flex items-center space-x-2 cursor-pointer">
8
+ <input type="checkbox" name="sidebar-categories" value="{{ value }}"
9
+ class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 sidebar-category-checkbox">
10
+ <span class="text-sm text-gray-600">{{ value }}</span>
11
+ </label>
12
+ {% endfor %}
13
+ </div>
14
+ <button id="update-categories-btn"
15
+ 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">
16
+ Update
17
+ </button>
18
+ </div>
19
+ </div>
@@ -1,419 +1,6 @@
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>
1
+ {% extends "base.html" %}
33
2
 
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 === " " ? "&nbsp;" : 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>
3
+ {% block content %}
4
+ {% include "components/setup_view.html" %}
5
+ {% include "components/quiz_view.html" %}
6
+ {% endblock %}
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ezquiz
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Add your description here
5
5
  Author-email: Alejandro Rodriguez <alejandro.rodriguez@alexanderthamm.com>
6
- Requires-Python: >=3.14
6
+ Requires-Python: >=3.12
7
7
  Requires-Dist: fastapi>=0.128.0
8
8
  Requires-Dist: jinja2>=3.1.6
9
9
  Requires-Dist: uvicorn>=0.40.0
@@ -0,0 +1,21 @@
1
+ ezquiz/__init__.py,sha256=_Ycs0PzsU0K53tGDxZMRP1kQ-TvIrF2_hTYNniOymxo,109
2
+ ezquiz/apigame.py,sha256=EVTuh_yzgaJzYIqfLz5G9kMhmuXeKtCS9V3YlBsAgV0,3073
3
+ ezquiz/cligame.py,sha256=h_bs4Cm-44ibzpacIycEGaprnFWYKsyTJuyoeaGEYRk,325
4
+ ezquiz/ezquiz.py,sha256=wlFp_bNcIvYGeAZjiibGnvtbvtDvbo-tD-9xBCNU08E,1393
5
+ ezquiz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ ezquiz/static/js/api.js,sha256=2cS5T9e-r-Z-xMUNXazqYDSLXF0oZ8EUoO7APu3IbqA,1205
7
+ ezquiz/static/js/diff.js,sha256=XkKUXCdPkeM8dV6SB3E5SQTFNWJLuGYd2rrn-QSy4Z0,2918
8
+ ezquiz/static/js/main.js,sha256=cd7Iy5Yy2ad3T4QU-DVFIDfgYeHNYBQYiiv8an6BB0k,306
9
+ ezquiz/static/js/state.js,sha256=-BYxQRlJEabrf1cGwAWnsKvM_tqYhYTIrJT_kOyS88U,692
10
+ ezquiz/static/js/views/quiz.js,sha256=4B2dBbviAlKYx6MnRSy0ljWNk9uUlGgyWIEXmF1a-k0,5266
11
+ ezquiz/static/js/views/results.js,sha256=W2y8tYDIimZvg3oRHR9xvrvHNWNMQfwMGU_DXhJtr1g,2572
12
+ ezquiz/static/js/views/setup.js,sha256=F7ieSzeewMfzxtsYz6E3kvkZbmR40o5TGZwH0KZAWgU,2103
13
+ ezquiz/static/js/views/sidebar.js,sha256=6YMU54R8CzuiNjWBdNNU0-a4cfr13wQrGN1dfIj9QAk,1429
14
+ ezquiz/templates/base.html,sha256=eURyfna3FsZ9GtA4t-XShBREubLOxX_vd0OXsGCOo20,1519
15
+ ezquiz/templates/index.html,sha256=rO5jY2Ljn5o7PiZB_d_MeQsiC4B1WBIPCsVwr_5h5ZA,155
16
+ ezquiz/templates/components/quiz_view.html,sha256=I--taugMtf_TubsdY56zTgZzwRAj2ao9k7k4ub7iOUU,1879
17
+ ezquiz/templates/components/setup_view.html,sha256=LzPY7OLI5rqwF1BLDqRtmk3uoitPuDD2ouwpxVVfyHQ,1023
18
+ ezquiz/templates/components/sidebar.html,sha256=xjxQpOEqTfGheQNLKtZcSsdKwgByEXA4pzMN4WVP3xo,931
19
+ ezquiz-0.3.0.dist-info/METADATA,sha256=EvxxF0vIHQuDf5Aadl0FxCOpgWAa6Qzq6kgSJZo9kps,276
20
+ ezquiz-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
21
+ ezquiz-0.3.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- ezquiz/__init__.py,sha256=_Ycs0PzsU0K53tGDxZMRP1kQ-TvIrF2_hTYNniOymxo,109
2
- ezquiz/apigame.py,sha256=bFuPDbf9oYRRiiemzTkoTaCJQixUdvlqorZJOZZhsTU,2552
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=iJnVwbbP1V-hEusURh8rUKya1MoMl6Yq-8MdOcsWAmI,17260
7
- ezquiz-0.2.1.dist-info/METADATA,sha256=AzH9WFdBq_bNODGnt3LSLosKaUiLHOxAnrXMBaIZbSk,276
8
- ezquiz-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
- ezquiz-0.2.1.dist-info/RECORD,,
File without changes