ezquiz 0.2.2__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 +14 -1
- ezquiz/ezquiz.py +16 -1
- ezquiz/static/js/api.js +51 -0
- ezquiz/static/js/diff.js +110 -0
- ezquiz/static/js/main.js +14 -0
- ezquiz/static/js/state.js +36 -0
- ezquiz/static/js/views/quiz.js +185 -0
- ezquiz/static/js/views/results.js +65 -0
- ezquiz/static/js/views/setup.js +85 -0
- ezquiz/static/js/views/sidebar.js +57 -0
- ezquiz/templates/base.html +57 -0
- ezquiz/templates/components/quiz_view.html +39 -0
- ezquiz/templates/components/setup_view.html +22 -0
- ezquiz/templates/components/sidebar.html +19 -0
- ezquiz/templates/index.html +5 -418
- {ezquiz-0.2.2.dist-info → ezquiz-0.3.0.dist-info}/METADATA +1 -1
- ezquiz-0.3.0.dist-info/RECORD +21 -0
- ezquiz-0.2.2.dist-info/RECORD +0 -9
- {ezquiz-0.2.2.dist-info → ezquiz-0.3.0.dist-info}/WHEEL +0 -0
ezquiz/apigame.py
CHANGED
|
@@ -4,6 +4,7 @@ from random import choice
|
|
|
4
4
|
import uvicorn
|
|
5
5
|
from fastapi import FastAPI, Request
|
|
6
6
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
7
|
+
from fastapi.staticfiles import StaticFiles
|
|
7
8
|
from fastapi.templating import Jinja2Templates
|
|
8
9
|
|
|
9
10
|
from ezquiz.ezquiz import Q
|
|
@@ -26,6 +27,11 @@ class APIGame:
|
|
|
26
27
|
**fastapi_kw,
|
|
27
28
|
):
|
|
28
29
|
app = FastAPI(**fastapi_kw)
|
|
30
|
+
app.mount(
|
|
31
|
+
"/static",
|
|
32
|
+
StaticFiles(directory=Path(__file__).parent / "static"),
|
|
33
|
+
name="static",
|
|
34
|
+
)
|
|
29
35
|
|
|
30
36
|
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
|
|
31
37
|
|
|
@@ -58,7 +64,14 @@ class APIGame:
|
|
|
58
64
|
return JSONResponse(
|
|
59
65
|
{
|
|
60
66
|
"complete": False,
|
|
61
|
-
"question": {
|
|
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
|
+
},
|
|
62
75
|
}
|
|
63
76
|
)
|
|
64
77
|
|
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],
|
|
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
|
+
)
|
ezquiz/static/js/api.js
ADDED
|
@@ -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
|
+
}
|
ezquiz/static/js/diff.js
ADDED
|
@@ -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 === " " ? " " : 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
|
+
}
|
ezquiz/static/js/main.js
ADDED
|
@@ -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>
|
ezquiz/templates/index.html
CHANGED
|
@@ -1,419 +1,6 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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>
|
|
3
|
+
{% block content %}
|
|
4
|
+
{% include "components/setup_view.html" %}
|
|
5
|
+
{% include "components/quiz_view.html" %}
|
|
6
|
+
{% endblock %}
|
|
@@ -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,,
|
ezquiz-0.2.2.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
ezquiz/__init__.py,sha256=_Ycs0PzsU0K53tGDxZMRP1kQ-TvIrF2_hTYNniOymxo,109
|
|
2
|
-
ezquiz/apigame.py,sha256=ljBYxvxkB2jyZYanxG5viso2mM2elOEnfvaVeyxsSnU,2596
|
|
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.2.dist-info/METADATA,sha256=xgwSS6h0_HgkS26cHzxqg22zmAWHIs44IxqhGqOHWXk,276
|
|
8
|
-
ezquiz-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
-
ezquiz-0.2.2.dist-info/RECORD,,
|
|
File without changes
|