milwright commited on
Commit
afac52e
·
1 Parent(s): 440ff08

Fix duplicate answer display bug

Browse files

- Remove redundant highlightAnswers() call in handleSubmit()
- Add duplicate check in revealAnswersInPlace() to prevent multiple answer spans
- Add unique class 'correct-answer-reveal' to answer spans for proper identification

CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ - Remember this command for launching this app in the future
docs/ai-prompts-and-parameters.md CHANGED
@@ -15,13 +15,16 @@ The game uses a level-based system to control difficulty:
15
  - **Levels 6-10**: 2 blanks per passage
16
  - **Level 11+**: 3 blanks per passage
17
 
18
- ### Word Length Constraints by Level
19
- - **Levels 1-3**: 3-10 letters (easier, shorter words)
20
- - **Levels 4+**: 5-13 letters (longer, more challenging words)
 
21
 
22
- ### Hint System by Level
23
- - **Levels 1-2**: Shows word length, first letter, and last letter
24
- - **Level 3+**: Shows word length and first letter only
 
 
25
 
26
  ## Request Types
27
 
@@ -72,7 +75,7 @@ The game uses a level-based system to control difficulty:
72
  "messages": [
73
  {
74
  "role": "user",
75
- "content": "You are a cluemaster vocabulary selector for educational cloze exercises. Select exactly [COUNT] words from this passage for a cloze exercise.\n\nCLOZE DELETION PRINCIPLES:\n- Select words that require understanding context and vocabulary to identify\n- Choose words essential for comprehension that test language ability\n- Target words where deletion creates meaningful cognitive gaps\n\nREQUIREMENTS:\n- Choose clear, properly-spelled words (no OCR errors like \"andsatires\")\n- Select meaningful nouns, verbs, or adjectives ([WORD_LENGTH] letters)\n- Words must appear EXACTLY as written in the passage\n- Avoid: capitalized words, function words, archaic terms, proper nouns, technical jargon\n- Skip any words that look malformed or concatenated\n\nReturn ONLY a JSON array of the selected words.\n\nPassage: \"[PASSAGE_TEXT]\""
76
  }
77
  ],
78
  "max_tokens": 100,
@@ -80,9 +83,8 @@ The game uses a level-based system to control difficulty:
80
  }
81
  ```
82
 
83
- **Word Length by Level:**
84
- - Levels 1-3: 3-10 letters
85
- - Levels 4+: 5-13 letters
86
 
87
  **Response Format:** JSON array of strings
88
  ```json
@@ -104,7 +106,7 @@ The game uses a level-based system to control difficulty:
104
  },
105
  {
106
  "role": "user",
107
- "content": "Process these two passages for cloze exercises:\n\nPASSAGE 1:\nTitle: \"[BOOK1_TITLE]\" by [BOOK1_AUTHOR]\nText: \"[PASSAGE1_TEXT]\"\nSelect [COUNT] words for blanks.\n\nPASSAGE 2:\nTitle: \"[BOOK2_TITLE]\" by [BOOK2_AUTHOR]\nText: \"[PASSAGE2_TEXT]\"\nSelect [COUNT] words for blanks.\n\nWORD SELECTION CRITERIA:\n[WORD_LENGTH_CRITERIA]\n- Choose meaningful nouns, verbs, or adjectives\n- Avoid capitalized words, function words, archaic terms\n- Words must appear EXACTLY as written in the passage\n\nFor each passage return:\n- \"words\": array of selected words (exactly as they appear)\n- \"context\": one-sentence intro about the book/author\n\nReturn as JSON: {\"passage1\": {...}, \"passage2\": {...}}"
108
  }
109
  ],
110
  "max_tokens": 800,
@@ -112,9 +114,9 @@ The game uses a level-based system to control difficulty:
112
  }
113
  ```
114
 
115
- **Word Length by Level:**
116
- - Levels 1-3: Select words 3-10 letters long
117
- - Levels 4+: Select words 5-13 letters long
118
 
119
  **Response Format:**
120
  ```json
@@ -186,10 +188,12 @@ All requests include these headers:
186
  ## Response Processing
187
 
188
  ### JSON Parsing Strategy
189
- 1. **Direct parsing**: Attempt to parse response as JSON
190
- 2. **Markdown extraction**: Extract JSON from code blocks (```json ... ```)
191
- 3. **Cleanup**: Remove markdown artifacts and fix truncated JSON
192
- 4. **Fallback extraction**: Use regex to extract partial data
 
 
193
 
194
  ### Artifact Removal
195
  All responses are cleaned to remove AI formatting artifacts:
@@ -214,14 +218,16 @@ content = content
214
 
215
  ### Model Choice
216
  - **Model**: `google/gemma-3-27b-it:free`
217
- - **Rationale**: Free tier model suitable for educational use
218
- - **Limitations**: Rate limiting requires batch processing and careful request management
 
219
 
220
  ### Rate Limiting Strategy
221
- 1. **Batch processing**: Combine multiple operations into single requests
222
- 2. **Two-passage system**: Process pairs of passages to reduce total API calls
223
- 3. **Fallback mechanisms**: Manual word selection when API unavailable
224
- 4. **Retry logic**: Handle temporary failures gracefully
 
225
 
226
  ### Security Considerations
227
  - API keys loaded from environment variables via meta tags
 
15
  - **Levels 6-10**: 2 blanks per passage
16
  - **Level 11+**: 3 blanks per passage
17
 
18
+ ### Level Progression Logic
19
+ - Players must pass **at least one passage** per round to advance levels
20
+ - Each round consists of two passages from different books
21
+ - Level advancement is determined after completing both passages
22
 
23
+ ### Word Selection Constraints
24
+ - **Word Length**: 4-12 letters for all levels
25
+ - **Avoid**: Capitalized words, ALL-CAPS words, function words, archaic terms, proper nouns, technical jargon
26
+ - **Placement**: Never select words from first or last sentence/clause of passages
27
+ - **Focus**: Choose words from middle portions for better context dependency
28
 
29
  ## Request Types
30
 
 
75
  "messages": [
76
  {
77
  "role": "user",
78
+ "content": "You are a cluemaster vocabulary selector for educational cloze exercises. Select exactly [COUNT] words from this passage for a cloze exercise.\n\nCLOZE DELETION PRINCIPLES:\n- Select words that require understanding context and vocabulary to identify\n- Choose words essential for comprehension that test language ability\n- Target words where deletion creates meaningful cognitive gaps\n\nREQUIREMENTS:\n- Choose clear, properly-spelled words (no OCR errors like \"andsatires\")\n- Select meaningful nouns, verbs, or adjectives (4-12 letters)\n- Words must appear EXACTLY as written in the passage\n- Avoid: capitalized words, ALL-CAPS words, function words, archaic terms, proper nouns, technical jargon\n- Skip any words that look malformed or concatenated\n- NEVER select words from the first or last sentence/clause of the passage\n- Choose words from the middle portions for better context dependency\n\nReturn ONLY a JSON array of the selected words.\n\nPassage: \"[PASSAGE_TEXT]\""
79
  }
80
  ],
81
  "max_tokens": 100,
 
83
  }
84
  ```
85
 
86
+ **Word Length Constraints:**
87
+ - All levels: 4-12 letters (consistent across all difficulty levels)
 
88
 
89
  **Response Format:** JSON array of strings
90
  ```json
 
106
  },
107
  {
108
  "role": "user",
109
+ "content": "Process these two passages for cloze exercises:\n\nPASSAGE 1:\nTitle: \"[BOOK1_TITLE]\" by [BOOK1_AUTHOR]\nText: \"[PASSAGE1_TEXT]\"\nSelect [COUNT] words for blanks.\n\nPASSAGE 2:\nTitle: \"[BOOK2_TITLE]\" by [BOOK2_AUTHOR]\nText: \"[PASSAGE2_TEXT]\"\nSelect [COUNT] words for blanks.\n\nSELECTION RULES:\n- Select EXACTLY [COUNT] word(s) per passage, no more, no less\n- Choose meaningful nouns, verbs, or adjectives (4-12 letters)\n- Avoid capitalized words, ALL-CAPS words, and table of contents entries\n- NEVER select words from the first or last sentence/clause of each passage\n- Choose words from the middle portions for better context dependency\n- Words must appear EXACTLY as written in the passage\n\nFor each passage return:\n- \"words\": array of EXACTLY [COUNT] selected word(s) (exactly as they appear in the text)\n- \"context\": one-sentence intro about the book/author\n\nCRITICAL: The \"words\" array must contain exactly [COUNT] element(s) for each passage.\n\nReturn as JSON: {\"passage1\": {...}, \"passage2\": {...}}"
110
  }
111
  ],
112
  "max_tokens": 800,
 
114
  }
115
  ```
116
 
117
+ **Word Selection Constraints:**
118
+ - All levels: 4-12 letters (consistent across all difficulty levels)
119
+ - Exact count enforcement with robust JSON parsing and error handling
120
 
121
  **Response Format:**
122
  ```json
 
188
  ## Response Processing
189
 
190
  ### JSON Parsing Strategy
191
+ 1. **Markdown cleanup**: Remove ```json and ``` wrappers
192
+ 2. **Trailing comma fixes**: Remove trailing commas from arrays (e.g., `["word",]` → `["word"]`)
193
+ 3. **Direct parsing**: Attempt to parse cleaned response as JSON
194
+ 4. **Structure validation**: Ensure required fields exist and are properly typed
195
+ 5. **Empty string filtering**: Remove empty strings from word arrays
196
+ 6. **Fallback extraction**: Use regex to extract partial data when parsing fails
197
 
198
  ### Artifact Removal
199
  All responses are cleaned to remove AI formatting artifacts:
 
218
 
219
  ### Model Choice
220
  - **Model**: `google/gemma-3-27b-it:free`
221
+ - **Rationale**: Free tier model suitable for educational use with good performance
222
+ - **Limitations**: Rate limiting and occasional JSON formatting issues
223
+ - **Performance**: Handles batch processing well with proper prompt engineering
224
 
225
  ### Rate Limiting Strategy
226
+ 1. **Batch processing**: Process two passages simultaneously in single API call
227
+ 2. **Round-based progression**: Two passages per round reduces API calls by 50%
228
+ 3. **Robust error handling**: JSON parsing fixes for malformed responses
229
+ 4. **Fallback mechanisms**: Sequential processing when batch fails
230
+ 5. **Retry logic**: Exponential backoff with 3 attempts for all requests
231
 
232
  ### Security Considerations
233
  - API keys loaded from environment variables via meta tags
favicon.ico ADDED
package-lock.json ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cloze-reader",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "cloze-reader",
9
+ "version": "1.0.0",
10
+ "license": "MIT",
11
+ "devDependencies": {
12
+ "http-server": "^14.1.1"
13
+ },
14
+ "engines": {
15
+ "node": ">=14.0.0",
16
+ "python": ">=3.9"
17
+ }
18
+ },
19
+ "node_modules/ansi-styles": {
20
+ "version": "4.3.0",
21
+ "dev": true,
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "color-convert": "^2.0.1"
25
+ },
26
+ "engines": {
27
+ "node": ">=8"
28
+ },
29
+ "funding": {
30
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
31
+ }
32
+ },
33
+ "node_modules/async": {
34
+ "version": "3.2.6",
35
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
36
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
37
+ "dev": true
38
+ },
39
+ "node_modules/basic-auth": {
40
+ "version": "2.0.1",
41
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
42
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
43
+ "dev": true,
44
+ "dependencies": {
45
+ "safe-buffer": "5.1.2"
46
+ },
47
+ "engines": {
48
+ "node": ">= 0.8"
49
+ }
50
+ },
51
+ "node_modules/basic-auth/node_modules/safe-buffer": {
52
+ "version": "5.1.2",
53
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
54
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
55
+ "dev": true
56
+ },
57
+ "node_modules/call-bind-apply-helpers": {
58
+ "version": "1.0.2",
59
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
60
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
61
+ "dev": true,
62
+ "dependencies": {
63
+ "es-errors": "^1.3.0",
64
+ "function-bind": "^1.1.2"
65
+ },
66
+ "engines": {
67
+ "node": ">= 0.4"
68
+ }
69
+ },
70
+ "node_modules/call-bound": {
71
+ "version": "1.0.4",
72
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
73
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
74
+ "dev": true,
75
+ "dependencies": {
76
+ "call-bind-apply-helpers": "^1.0.2",
77
+ "get-intrinsic": "^1.3.0"
78
+ },
79
+ "engines": {
80
+ "node": ">= 0.4"
81
+ },
82
+ "funding": {
83
+ "url": "https://github.com/sponsors/ljharb"
84
+ }
85
+ },
86
+ "node_modules/chalk": {
87
+ "version": "4.1.2",
88
+ "dev": true,
89
+ "license": "MIT",
90
+ "dependencies": {
91
+ "ansi-styles": "^4.1.0",
92
+ "supports-color": "^7.1.0"
93
+ },
94
+ "engines": {
95
+ "node": ">=10"
96
+ },
97
+ "funding": {
98
+ "url": "https://github.com/chalk/chalk?sponsor=1"
99
+ }
100
+ },
101
+ "node_modules/color-convert": {
102
+ "version": "2.0.1",
103
+ "dev": true,
104
+ "license": "MIT",
105
+ "dependencies": {
106
+ "color-name": "~1.1.4"
107
+ },
108
+ "engines": {
109
+ "node": ">=7.0.0"
110
+ }
111
+ },
112
+ "node_modules/color-name": {
113
+ "version": "1.1.4",
114
+ "dev": true,
115
+ "license": "MIT"
116
+ },
117
+ "node_modules/corser": {
118
+ "version": "2.0.1",
119
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
120
+ "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
121
+ "dev": true,
122
+ "engines": {
123
+ "node": ">= 0.4.0"
124
+ }
125
+ },
126
+ "node_modules/debug": {
127
+ "version": "4.4.1",
128
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
129
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
130
+ "dev": true,
131
+ "dependencies": {
132
+ "ms": "^2.1.3"
133
+ },
134
+ "engines": {
135
+ "node": ">=6.0"
136
+ },
137
+ "peerDependenciesMeta": {
138
+ "supports-color": {
139
+ "optional": true
140
+ }
141
+ }
142
+ },
143
+ "node_modules/dunder-proto": {
144
+ "version": "1.0.1",
145
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
146
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
147
+ "dev": true,
148
+ "dependencies": {
149
+ "call-bind-apply-helpers": "^1.0.1",
150
+ "es-errors": "^1.3.0",
151
+ "gopd": "^1.2.0"
152
+ },
153
+ "engines": {
154
+ "node": ">= 0.4"
155
+ }
156
+ },
157
+ "node_modules/es-define-property": {
158
+ "version": "1.0.1",
159
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
160
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
161
+ "dev": true,
162
+ "engines": {
163
+ "node": ">= 0.4"
164
+ }
165
+ },
166
+ "node_modules/es-errors": {
167
+ "version": "1.3.0",
168
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
169
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
170
+ "dev": true,
171
+ "engines": {
172
+ "node": ">= 0.4"
173
+ }
174
+ },
175
+ "node_modules/es-object-atoms": {
176
+ "version": "1.1.1",
177
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
178
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
179
+ "dev": true,
180
+ "dependencies": {
181
+ "es-errors": "^1.3.0"
182
+ },
183
+ "engines": {
184
+ "node": ">= 0.4"
185
+ }
186
+ },
187
+ "node_modules/eventemitter3": {
188
+ "version": "4.0.7",
189
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
190
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
191
+ "dev": true
192
+ },
193
+ "node_modules/follow-redirects": {
194
+ "version": "1.15.9",
195
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
196
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
197
+ "dev": true,
198
+ "funding": [
199
+ {
200
+ "type": "individual",
201
+ "url": "https://github.com/sponsors/RubenVerborgh"
202
+ }
203
+ ],
204
+ "engines": {
205
+ "node": ">=4.0"
206
+ },
207
+ "peerDependenciesMeta": {
208
+ "debug": {
209
+ "optional": true
210
+ }
211
+ }
212
+ },
213
+ "node_modules/function-bind": {
214
+ "version": "1.1.2",
215
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
216
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
217
+ "dev": true,
218
+ "funding": {
219
+ "url": "https://github.com/sponsors/ljharb"
220
+ }
221
+ },
222
+ "node_modules/get-intrinsic": {
223
+ "version": "1.3.0",
224
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
225
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
226
+ "dev": true,
227
+ "dependencies": {
228
+ "call-bind-apply-helpers": "^1.0.2",
229
+ "es-define-property": "^1.0.1",
230
+ "es-errors": "^1.3.0",
231
+ "es-object-atoms": "^1.1.1",
232
+ "function-bind": "^1.1.2",
233
+ "get-proto": "^1.0.1",
234
+ "gopd": "^1.2.0",
235
+ "has-symbols": "^1.1.0",
236
+ "hasown": "^2.0.2",
237
+ "math-intrinsics": "^1.1.0"
238
+ },
239
+ "engines": {
240
+ "node": ">= 0.4"
241
+ },
242
+ "funding": {
243
+ "url": "https://github.com/sponsors/ljharb"
244
+ }
245
+ },
246
+ "node_modules/get-proto": {
247
+ "version": "1.0.1",
248
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
249
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
250
+ "dev": true,
251
+ "dependencies": {
252
+ "dunder-proto": "^1.0.1",
253
+ "es-object-atoms": "^1.0.0"
254
+ },
255
+ "engines": {
256
+ "node": ">= 0.4"
257
+ }
258
+ },
259
+ "node_modules/gopd": {
260
+ "version": "1.2.0",
261
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
262
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
263
+ "dev": true,
264
+ "engines": {
265
+ "node": ">= 0.4"
266
+ },
267
+ "funding": {
268
+ "url": "https://github.com/sponsors/ljharb"
269
+ }
270
+ },
271
+ "node_modules/has-flag": {
272
+ "version": "4.0.0",
273
+ "dev": true,
274
+ "license": "MIT",
275
+ "engines": {
276
+ "node": ">=8"
277
+ }
278
+ },
279
+ "node_modules/has-symbols": {
280
+ "version": "1.1.0",
281
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
282
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
283
+ "dev": true,
284
+ "engines": {
285
+ "node": ">= 0.4"
286
+ },
287
+ "funding": {
288
+ "url": "https://github.com/sponsors/ljharb"
289
+ }
290
+ },
291
+ "node_modules/hasown": {
292
+ "version": "2.0.2",
293
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
294
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
295
+ "dev": true,
296
+ "dependencies": {
297
+ "function-bind": "^1.1.2"
298
+ },
299
+ "engines": {
300
+ "node": ">= 0.4"
301
+ }
302
+ },
303
+ "node_modules/he": {
304
+ "version": "1.2.0",
305
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
306
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
307
+ "dev": true,
308
+ "bin": {
309
+ "he": "bin/he"
310
+ }
311
+ },
312
+ "node_modules/html-encoding-sniffer": {
313
+ "version": "3.0.0",
314
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
315
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
316
+ "dev": true,
317
+ "dependencies": {
318
+ "whatwg-encoding": "^2.0.0"
319
+ },
320
+ "engines": {
321
+ "node": ">=12"
322
+ }
323
+ },
324
+ "node_modules/http-proxy": {
325
+ "version": "1.18.1",
326
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
327
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
328
+ "dev": true,
329
+ "dependencies": {
330
+ "eventemitter3": "^4.0.0",
331
+ "follow-redirects": "^1.0.0",
332
+ "requires-port": "^1.0.0"
333
+ },
334
+ "engines": {
335
+ "node": ">=8.0.0"
336
+ }
337
+ },
338
+ "node_modules/http-server": {
339
+ "version": "14.1.1",
340
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
341
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
342
+ "dev": true,
343
+ "dependencies": {
344
+ "basic-auth": "^2.0.1",
345
+ "chalk": "^4.1.2",
346
+ "corser": "^2.0.1",
347
+ "he": "^1.2.0",
348
+ "html-encoding-sniffer": "^3.0.0",
349
+ "http-proxy": "^1.18.1",
350
+ "mime": "^1.6.0",
351
+ "minimist": "^1.2.6",
352
+ "opener": "^1.5.1",
353
+ "portfinder": "^1.0.28",
354
+ "secure-compare": "3.0.1",
355
+ "union": "~0.5.0",
356
+ "url-join": "^4.0.1"
357
+ },
358
+ "bin": {
359
+ "http-server": "bin/http-server"
360
+ },
361
+ "engines": {
362
+ "node": ">=12"
363
+ }
364
+ },
365
+ "node_modules/iconv-lite": {
366
+ "version": "0.6.3",
367
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
368
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
369
+ "dev": true,
370
+ "dependencies": {
371
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
372
+ },
373
+ "engines": {
374
+ "node": ">=0.10.0"
375
+ }
376
+ },
377
+ "node_modules/math-intrinsics": {
378
+ "version": "1.1.0",
379
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
380
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
381
+ "dev": true,
382
+ "engines": {
383
+ "node": ">= 0.4"
384
+ }
385
+ },
386
+ "node_modules/mime": {
387
+ "version": "1.6.0",
388
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
389
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
390
+ "dev": true,
391
+ "bin": {
392
+ "mime": "cli.js"
393
+ },
394
+ "engines": {
395
+ "node": ">=4"
396
+ }
397
+ },
398
+ "node_modules/minimist": {
399
+ "version": "1.2.8",
400
+ "dev": true,
401
+ "license": "MIT",
402
+ "funding": {
403
+ "url": "https://github.com/sponsors/ljharb"
404
+ }
405
+ },
406
+ "node_modules/ms": {
407
+ "version": "2.1.3",
408
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
409
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
410
+ "dev": true
411
+ },
412
+ "node_modules/object-inspect": {
413
+ "version": "1.13.4",
414
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
415
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
416
+ "dev": true,
417
+ "engines": {
418
+ "node": ">= 0.4"
419
+ },
420
+ "funding": {
421
+ "url": "https://github.com/sponsors/ljharb"
422
+ }
423
+ },
424
+ "node_modules/opener": {
425
+ "version": "1.5.2",
426
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
427
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
428
+ "dev": true,
429
+ "bin": {
430
+ "opener": "bin/opener-bin.js"
431
+ }
432
+ },
433
+ "node_modules/portfinder": {
434
+ "version": "1.0.37",
435
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz",
436
+ "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==",
437
+ "dev": true,
438
+ "dependencies": {
439
+ "async": "^3.2.6",
440
+ "debug": "^4.3.6"
441
+ },
442
+ "engines": {
443
+ "node": ">= 10.12"
444
+ }
445
+ },
446
+ "node_modules/qs": {
447
+ "version": "6.14.0",
448
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
449
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
450
+ "dev": true,
451
+ "dependencies": {
452
+ "side-channel": "^1.1.0"
453
+ },
454
+ "engines": {
455
+ "node": ">=0.6"
456
+ },
457
+ "funding": {
458
+ "url": "https://github.com/sponsors/ljharb"
459
+ }
460
+ },
461
+ "node_modules/requires-port": {
462
+ "version": "1.0.0",
463
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
464
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
465
+ "dev": true
466
+ },
467
+ "node_modules/safer-buffer": {
468
+ "version": "2.1.2",
469
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
470
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
471
+ "dev": true
472
+ },
473
+ "node_modules/secure-compare": {
474
+ "version": "3.0.1",
475
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
476
+ "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
477
+ "dev": true
478
+ },
479
+ "node_modules/side-channel": {
480
+ "version": "1.1.0",
481
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
482
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
483
+ "dev": true,
484
+ "dependencies": {
485
+ "es-errors": "^1.3.0",
486
+ "object-inspect": "^1.13.3",
487
+ "side-channel-list": "^1.0.0",
488
+ "side-channel-map": "^1.0.1",
489
+ "side-channel-weakmap": "^1.0.2"
490
+ },
491
+ "engines": {
492
+ "node": ">= 0.4"
493
+ },
494
+ "funding": {
495
+ "url": "https://github.com/sponsors/ljharb"
496
+ }
497
+ },
498
+ "node_modules/side-channel-list": {
499
+ "version": "1.0.0",
500
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
501
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
502
+ "dev": true,
503
+ "dependencies": {
504
+ "es-errors": "^1.3.0",
505
+ "object-inspect": "^1.13.3"
506
+ },
507
+ "engines": {
508
+ "node": ">= 0.4"
509
+ },
510
+ "funding": {
511
+ "url": "https://github.com/sponsors/ljharb"
512
+ }
513
+ },
514
+ "node_modules/side-channel-map": {
515
+ "version": "1.0.1",
516
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
517
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
518
+ "dev": true,
519
+ "dependencies": {
520
+ "call-bound": "^1.0.2",
521
+ "es-errors": "^1.3.0",
522
+ "get-intrinsic": "^1.2.5",
523
+ "object-inspect": "^1.13.3"
524
+ },
525
+ "engines": {
526
+ "node": ">= 0.4"
527
+ },
528
+ "funding": {
529
+ "url": "https://github.com/sponsors/ljharb"
530
+ }
531
+ },
532
+ "node_modules/side-channel-weakmap": {
533
+ "version": "1.0.2",
534
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
535
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
536
+ "dev": true,
537
+ "dependencies": {
538
+ "call-bound": "^1.0.2",
539
+ "es-errors": "^1.3.0",
540
+ "get-intrinsic": "^1.2.5",
541
+ "object-inspect": "^1.13.3",
542
+ "side-channel-map": "^1.0.1"
543
+ },
544
+ "engines": {
545
+ "node": ">= 0.4"
546
+ },
547
+ "funding": {
548
+ "url": "https://github.com/sponsors/ljharb"
549
+ }
550
+ },
551
+ "node_modules/supports-color": {
552
+ "version": "7.2.0",
553
+ "dev": true,
554
+ "license": "MIT",
555
+ "dependencies": {
556
+ "has-flag": "^4.0.0"
557
+ },
558
+ "engines": {
559
+ "node": ">=8"
560
+ }
561
+ },
562
+ "node_modules/union": {
563
+ "version": "0.5.0",
564
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
565
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
566
+ "dev": true,
567
+ "dependencies": {
568
+ "qs": "^6.4.0"
569
+ },
570
+ "engines": {
571
+ "node": ">= 0.8.0"
572
+ }
573
+ },
574
+ "node_modules/url-join": {
575
+ "version": "4.0.1",
576
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
577
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
578
+ "dev": true
579
+ },
580
+ "node_modules/whatwg-encoding": {
581
+ "version": "2.0.0",
582
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
583
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
584
+ "dev": true,
585
+ "dependencies": {
586
+ "iconv-lite": "0.6.3"
587
+ },
588
+ "engines": {
589
+ "node": ">=12"
590
+ }
591
+ }
592
+ }
593
+ }
src/app.js CHANGED
@@ -150,7 +150,6 @@ class App {
150
  // Submit answers and get results
151
  this.currentResults = this.game.submitAnswers(answers);
152
  this.displayResults(this.currentResults);
153
- this.highlightAnswers(this.currentResults.results);
154
  }
155
 
156
  displayResults(results) {
@@ -253,11 +252,14 @@ class App {
253
  input.style.backgroundColor = '#fef2f2'; // Light red
254
  input.style.borderColor = '#dc2626'; // Red border
255
 
256
- // Show correct answer below the input
257
- const correctAnswerSpan = document.createElement('span');
258
- correctAnswerSpan.className = 'text-sm text-green-600 font-semibold ml-2';
259
- correctAnswerSpan.textContent = `✓ ${result.correctAnswer}`;
260
- input.parentNode.appendChild(correctAnswerSpan);
 
 
 
261
  }
262
  input.disabled = true;
263
  }
 
150
  // Submit answers and get results
151
  this.currentResults = this.game.submitAnswers(answers);
152
  this.displayResults(this.currentResults);
 
153
  }
154
 
155
  displayResults(results) {
 
252
  input.style.backgroundColor = '#fef2f2'; // Light red
253
  input.style.borderColor = '#dc2626'; // Red border
254
 
255
+ // Show correct answer below the input (only if not already shown)
256
+ const existingAnswer = input.parentNode.querySelector('.correct-answer-reveal');
257
+ if (!existingAnswer) {
258
+ const correctAnswerSpan = document.createElement('span');
259
+ correctAnswerSpan.className = 'correct-answer-reveal text-sm text-green-600 font-semibold ml-2';
260
+ correctAnswerSpan.textContent = `✓ ${result.correctAnswer}`;
261
+ input.parentNode.appendChild(correctAnswerSpan);
262
+ }
263
  }
264
  input.disabled = true;
265
  }
test-blanks.html ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Test Blanks Count</title>
7
+ </head>
8
+ <body>
9
+ <h1>Testing Blanks Count</h1>
10
+ <div id="output"></div>
11
+
12
+ <script type="module">
13
+ import ClozeGame from './src/clozeGameEngine.js';
14
+
15
+ async function testBlanksCount() {
16
+ const output = document.getElementById('output');
17
+ const game = new ClozeGame();
18
+
19
+ try {
20
+ await game.initialize();
21
+ output.innerHTML += '<p>Game initialized</p>';
22
+
23
+ // Test level 1 (should have 1 blank)
24
+ game.currentLevel = 1;
25
+ const round1 = await game.startNewRound();
26
+ output.innerHTML += `<p>Level 1: ${round1.blanks.length} blanks</p>`;
27
+
28
+ // Test level 5 (should have 1 blank)
29
+ game.currentLevel = 5;
30
+ const round5 = await game.startNewRound();
31
+ output.innerHTML += `<p>Level 5: ${round5.blanks.length} blanks</p>`;
32
+
33
+ // Test level 6 (should have 2 blanks)
34
+ game.currentLevel = 6;
35
+ const round6 = await game.startNewRound();
36
+ output.innerHTML += `<p>Level 6: ${round6.blanks.length} blanks</p>`;
37
+
38
+ // Test level 11 (should have 3 blanks)
39
+ game.currentLevel = 11;
40
+ const round11 = await game.startNewRound();
41
+ output.innerHTML += `<p>Level 11: ${round11.blanks.length} blanks</p>`;
42
+
43
+ } catch (error) {
44
+ output.innerHTML += `<p style="color: red;">Error: ${error.message}</p>`;
45
+ console.error(error);
46
+ }
47
+ }
48
+
49
+ testBlanksCount();
50
+ </script>
51
+ </body>
52
+ </html>
test-duplication-fix.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Test Duplication Fix</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
9
+ <body class="bg-gray-100 p-8">
10
+ <div class="max-w-4xl mx-auto">
11
+ <h1 class="text-2xl font-bold mb-4">Testing Answer Duplication Fix</h1>
12
+
13
+ <div id="app"></div>
14
+ </div>
15
+
16
+ <script type="module">
17
+ import ClozeGameEngine from './src/clozeGameEngine.js';
18
+ import ClozeGameUI from './src/app.js';
19
+
20
+ // Initialize the game
21
+ const game = new ClozeGameEngine();
22
+ const ui = new ClozeGameUI(game);
23
+
24
+ console.log('Game initialized. Try submitting answers multiple times to test for duplication.');
25
+ </script>
26
+ </body>
27
+ </html>