Spaces:
Running
Running
fix level progression and optimize ai word selection
Browse files- Fix level progression bug where advancement only considered second passage
- Add round-level result tracking for proper level advancement
- Improve AI prompts to prevent selection from first/last clauses
- Add JSON parsing robustness for trailing commas and malformed arrays
- Enhance word selection criteria to avoid ALL-CAPS and table contents
- Update difficulty scaling: 1-5 (1 blank), 6-10 (2 blanks), 11+ (3 blanks)
- Optimize batch processing with exact word count requirements
- docs/ai-prompts-and-parameters.md +33 -8
- index.html +1 -0
- package.json +1 -2
- src/aiService.js +21 -2
- src/app.js +1 -1
- src/clozeGameEngine.js +68 -19
- src/init-env.js +0 -2
docs/ai-prompts-and-parameters.md
CHANGED
@@ -6,6 +6,23 @@ This document outlines the different types of AI requests, prompts, and paramete
|
|
6 |
|
7 |
The Cloze Reader uses OpenRouter's API with the `google/gemma-3-27b-it:free` model to power various AI-driven features. All requests use a consistent retry mechanism with exponential backoff (3 attempts, 0.5s initial delay).
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
## Request Types
|
10 |
|
11 |
### 1. Contextual Hint Generation
|
@@ -55,14 +72,18 @@ The Cloze Reader uses OpenRouter's API with the `google/gemma-3-27b-it:free` mod
|
|
55 |
"messages": [
|
56 |
{
|
57 |
"role": "user",
|
58 |
-
"content": "You are a cluemaster vocabulary selector for educational cloze exercises. Select exactly [COUNT] words from this passage for a cloze exercise.\n\nREQUIREMENTS:\n- Choose clear, properly-spelled words (no OCR errors like \"andsatires\")\n- Select meaningful nouns, verbs, or adjectives (
|
59 |
}
|
60 |
],
|
61 |
"max_tokens": 100,
|
62 |
-
"temperature": 0.
|
63 |
}
|
64 |
```
|
65 |
|
|
|
|
|
|
|
|
|
66 |
**Response Format:** JSON array of strings
|
67 |
```json
|
68 |
["word1", "word2", "word3"]
|
@@ -83,14 +104,18 @@ The Cloze Reader uses OpenRouter's API with the `google/gemma-3-27b-it:free` mod
|
|
83 |
},
|
84 |
{
|
85 |
"role": "user",
|
86 |
-
"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\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\": {...}}"
|
87 |
}
|
88 |
],
|
89 |
-
"max_tokens":
|
90 |
"temperature": 0.5
|
91 |
}
|
92 |
```
|
93 |
|
|
|
|
|
|
|
|
|
94 |
**Response Format:**
|
95 |
```json
|
96 |
{
|
@@ -150,12 +175,12 @@ All requests include these headers:
|
|
150 |
|---------|------------|-------------|-------------|
|
151 |
| Hints | 50 | 0.6 | 3 attempts |
|
152 |
| Word Selection | 100 | 0.3 | 3 attempts |
|
153 |
-
| Batch Processing |
|
154 |
-
| Contextualization | 80 | 0.
|
155 |
|
156 |
### Temperature Guidelines
|
157 |
-
- **0.
|
158 |
-
- **0.
|
159 |
- **0.6**: Creative tasks (hint generation)
|
160 |
|
161 |
## Response Processing
|
|
|
6 |
|
7 |
The Cloze Reader uses OpenRouter's API with the `google/gemma-3-27b-it:free` model to power various AI-driven features. All requests use a consistent retry mechanism with exponential backoff (3 attempts, 0.5s initial delay).
|
8 |
|
9 |
+
## Difficulty Progression
|
10 |
+
|
11 |
+
The game uses a level-based system to control difficulty:
|
12 |
+
|
13 |
+
### Blank Count by Level
|
14 |
+
- **Levels 1-5**: 1 blank per passage
|
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 |
|
28 |
### 1. Contextual Hint Generation
|
|
|
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,
|
79 |
+
"temperature": 0.3
|
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
|
89 |
["word1", "word2", "word3"]
|
|
|
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,
|
111 |
"temperature": 0.5
|
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
|
121 |
{
|
|
|
175 |
|---------|------------|-------------|-------------|
|
176 |
| Hints | 50 | 0.6 | 3 attempts |
|
177 |
| Word Selection | 100 | 0.3 | 3 attempts |
|
178 |
+
| Batch Processing | 800 | 0.5 | 3 attempts |
|
179 |
+
| Contextualization | 80 | 0.5 | 3 attempts |
|
180 |
|
181 |
### Temperature Guidelines
|
182 |
+
- **0.3**: Structured tasks (word selection)
|
183 |
+
- **0.5**: Semi-structured tasks (batch processing, contextualization)
|
184 |
- **0.6**: Creative tasks (hint generation)
|
185 |
|
186 |
## Response Processing
|
index.html
CHANGED
@@ -7,6 +7,7 @@
|
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
<link href="https://fonts.googleapis.com/css2?family=Special+Elite&display=swap" rel="stylesheet">
|
9 |
<link href="./src/styles.css" rel="stylesheet">
|
|
|
10 |
</head>
|
11 |
<body class="min-h-screen">
|
12 |
<div id="app" class="container mx-auto px-4 py-8 max-w-4xl">
|
|
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
<link href="https://fonts.googleapis.com/css2?family=Special+Elite&display=swap" rel="stylesheet">
|
9 |
<link href="./src/styles.css" rel="stylesheet">
|
10 |
+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
11 |
</head>
|
12 |
<body class="min-h-screen">
|
13 |
<div id="app" class="container mx-auto px-4 py-8 max-w-4xl">
|
package.json
CHANGED
@@ -30,9 +30,8 @@
|
|
30 |
"devDependencies": {
|
31 |
"http-server": "^14.1.1"
|
32 |
},
|
33 |
-
"dependencies": {},
|
34 |
"engines": {
|
35 |
"node": ">=14.0.0",
|
36 |
"python": ">=3.9"
|
37 |
}
|
38 |
-
}
|
|
|
30 |
"devDependencies": {
|
31 |
"http-server": "^14.1.1"
|
32 |
},
|
|
|
33 |
"engines": {
|
34 |
"node": ">=14.0.0",
|
35 |
"python": ">=3.9"
|
36 |
}
|
37 |
+
}
|
src/aiService.js
CHANGED
@@ -151,8 +151,10 @@ REQUIREMENTS:
|
|
151 |
- Choose clear, properly-spelled words (no OCR errors like "andsatires")
|
152 |
- Select meaningful nouns, verbs, or adjectives (4-12 letters)
|
153 |
- Words must appear EXACTLY as written in the passage
|
154 |
-
- Avoid: capitalized words, function words, archaic terms, proper nouns, technical jargon
|
155 |
- Skip any words that look malformed or concatenated
|
|
|
|
|
156 |
|
157 |
Return ONLY a JSON array of the selected words.
|
158 |
|
@@ -243,10 +245,20 @@ Title: "${book2.title}" by ${book2.author}
|
|
243 |
Text: "${passage2}"
|
244 |
Select ${blanksPerPassage} words for blanks.
|
245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
For each passage return:
|
247 |
-
- "words": array of selected
|
248 |
- "context": one-sentence intro about the book/author
|
249 |
|
|
|
|
|
250 |
Return as JSON: {"passage1": {...}, "passage2": {...}}`
|
251 |
}],
|
252 |
max_tokens: 800,
|
@@ -287,6 +299,9 @@ Return as JSON: {"passage1": {...}, "passage2": {...}}`
|
|
287 |
.trim();
|
288 |
|
289 |
// Try to fix common JSON issues
|
|
|
|
|
|
|
290 |
// Check for truncated strings (unterminated quotes)
|
291 |
const quoteCount = (jsonString.match(/"/g) || []).length;
|
292 |
if (quoteCount % 2 !== 0) {
|
@@ -325,6 +340,10 @@ Return as JSON: {"passage1": {...}, "passage2": {...}}`
|
|
325 |
parsed.passage2.words = [];
|
326 |
}
|
327 |
|
|
|
|
|
|
|
|
|
328 |
return parsed;
|
329 |
} catch (e) {
|
330 |
console.error('Failed to parse batch response:', e);
|
|
|
151 |
- Choose clear, properly-spelled words (no OCR errors like "andsatires")
|
152 |
- Select meaningful nouns, verbs, or adjectives (4-12 letters)
|
153 |
- Words must appear EXACTLY as written in the passage
|
154 |
+
- Avoid: capitalized words, ALL-CAPS words, function words, archaic terms, proper nouns, technical jargon
|
155 |
- Skip any words that look malformed or concatenated
|
156 |
+
- NEVER select words from the first or last sentence/clause of the passage
|
157 |
+
- Choose words from the middle portions for better context dependency
|
158 |
|
159 |
Return ONLY a JSON array of the selected words.
|
160 |
|
|
|
245 |
Text: "${passage2}"
|
246 |
Select ${blanksPerPassage} words for blanks.
|
247 |
|
248 |
+
SELECTION RULES:
|
249 |
+
- Select EXACTLY ${blanksPerPassage} word${blanksPerPassage > 1 ? 's' : ''} per passage, no more, no less
|
250 |
+
- Choose meaningful nouns, verbs, or adjectives (4-12 letters)
|
251 |
+
- Avoid capitalized words, ALL-CAPS words, and table of contents entries
|
252 |
+
- NEVER select words from the first or last sentence/clause of each passage
|
253 |
+
- Choose words from the middle portions for better context dependency
|
254 |
+
- Words must appear EXACTLY as written in the passage
|
255 |
+
|
256 |
For each passage return:
|
257 |
+
- "words": array of EXACTLY ${blanksPerPassage} selected word${blanksPerPassage > 1 ? 's' : ''} (exactly as they appear in the text)
|
258 |
- "context": one-sentence intro about the book/author
|
259 |
|
260 |
+
CRITICAL: The "words" array must contain exactly ${blanksPerPassage} element${blanksPerPassage > 1 ? 's' : ''} for each passage.
|
261 |
+
|
262 |
Return as JSON: {"passage1": {...}, "passage2": {...}}`
|
263 |
}],
|
264 |
max_tokens: 800,
|
|
|
299 |
.trim();
|
300 |
|
301 |
// Try to fix common JSON issues
|
302 |
+
// Fix trailing commas in arrays
|
303 |
+
jsonString = jsonString.replace(/,(\s*])/g, '$1');
|
304 |
+
|
305 |
// Check for truncated strings (unterminated quotes)
|
306 |
const quoteCount = (jsonString.match(/"/g) || []).length;
|
307 |
if (quoteCount % 2 !== 0) {
|
|
|
340 |
parsed.passage2.words = [];
|
341 |
}
|
342 |
|
343 |
+
// Filter out empty strings from words arrays (caused by trailing commas)
|
344 |
+
parsed.passage1.words = parsed.passage1.words.filter(word => word && word.trim() !== '');
|
345 |
+
parsed.passage2.words = parsed.passage2.words.filter(word => word && word.trim() !== '');
|
346 |
+
|
347 |
return parsed;
|
348 |
} catch (e) {
|
349 |
console.error('Failed to parse batch response:', e);
|
src/app.js
CHANGED
@@ -204,7 +204,7 @@ class App {
|
|
204 |
async handleNext() {
|
205 |
try {
|
206 |
// Show loading immediately with specific message
|
207 |
-
this.showLoading(true, 'Loading
|
208 |
|
209 |
// Clear chat history when starting new passage/round
|
210 |
this.chatUI.clearChatHistory();
|
|
|
204 |
async handleNext() {
|
205 |
try {
|
206 |
// Show loading immediately with specific message
|
207 |
+
this.showLoading(true, 'Loading passages...');
|
208 |
|
209 |
// Clear chat history when starting new passage/round
|
210 |
this.chatUI.clearChatHistory();
|
src/clozeGameEngine.js
CHANGED
@@ -19,6 +19,7 @@ class ClozeGame {
|
|
19 |
this.hints = [];
|
20 |
this.chatService = new ChatService(aiService);
|
21 |
this.lastResults = null; // Store results for answer revelation
|
|
|
22 |
|
23 |
// Two-passage system properties
|
24 |
this.currentBooks = []; // Array of two books per round
|
@@ -52,11 +53,11 @@ class ClozeGame {
|
|
52 |
this.currentPassageIndex = 0;
|
53 |
|
54 |
// Calculate blanks per passage based on level
|
55 |
-
//
|
56 |
let blanksPerPassage;
|
57 |
-
if (this.currentLevel <=
|
58 |
blanksPerPassage = 1;
|
59 |
-
} else if (this.currentLevel <=
|
60 |
blanksPerPassage = 2;
|
61 |
} else {
|
62 |
blanksPerPassage = 3;
|
@@ -65,12 +66,17 @@ class ClozeGame {
|
|
65 |
// Process both passages in a single API call
|
66 |
try {
|
67 |
const batchResult = await aiService.processBothPassages(
|
68 |
-
passage1, book1, passage2, book2, blanksPerPassage
|
69 |
);
|
70 |
|
71 |
// Store the preprocessed data for both passages
|
72 |
this.preprocessedData = batchResult;
|
73 |
|
|
|
|
|
|
|
|
|
|
|
74 |
// Set up first passage using preprocessed data
|
75 |
this.currentBook = book1;
|
76 |
this.originalText = this.passages[0];
|
@@ -186,20 +192,49 @@ class ClozeGame {
|
|
186 |
async createClozeTextFromPreprocessed(passageIndex) {
|
187 |
// Use preprocessed word selection from batch API call
|
188 |
const preprocessed = passageIndex === 0 ? this.preprocessedData.passage1 : this.preprocessedData.passage2;
|
189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
|
191 |
// Split passage into words
|
192 |
const words = this.originalText.split(/(\s+)/);
|
193 |
const wordsOnly = words.filter(w => w.trim() !== '');
|
194 |
|
195 |
-
// Find indices of selected words
|
196 |
const selectedIndices = [];
|
197 |
selectedWords.forEach(word => {
|
198 |
-
|
199 |
-
|
200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
if (index !== -1) {
|
202 |
selectedIndices.push(index);
|
|
|
|
|
203 |
}
|
204 |
});
|
205 |
|
@@ -208,6 +243,8 @@ class ClozeGame {
|
|
208 |
this.hints = [];
|
209 |
const clozeWords = [...wordsOnly];
|
210 |
|
|
|
|
|
211 |
selectedIndices.forEach((wordIndex, blankIndex) => {
|
212 |
const originalWord = wordsOnly[wordIndex];
|
213 |
const cleanWord = originalWord.replace(/[^\w]/g, '');
|
@@ -259,11 +296,11 @@ class ClozeGame {
|
|
259 |
|
260 |
async createClozeText() {
|
261 |
const words = this.originalText.split(' ');
|
262 |
-
// Progressive difficulty: levels 1-
|
263 |
let numberOfBlanks;
|
264 |
-
if (this.currentLevel <=
|
265 |
numberOfBlanks = 1;
|
266 |
-
} else if (this.currentLevel <=
|
267 |
numberOfBlanks = 2;
|
268 |
} else {
|
269 |
numberOfBlanks = 3;
|
@@ -277,7 +314,8 @@ class ClozeGame {
|
|
277 |
try {
|
278 |
significantWords = await aiService.selectSignificantWords(
|
279 |
this.originalText,
|
280 |
-
numberOfBlanks
|
|
|
281 |
);
|
282 |
console.log('AI selected words:', significantWords);
|
283 |
} catch (error) {
|
@@ -580,6 +618,9 @@ class ClozeGame {
|
|
580 |
|
581 |
// Store results for potential answer revelation
|
582 |
this.lastResults = resultsData;
|
|
|
|
|
|
|
583 |
|
584 |
return resultsData;
|
585 |
}
|
@@ -616,7 +657,7 @@ class ClozeGame {
|
|
616 |
// Clear chat conversations for new passage
|
617 |
this.chatService.clearConversations();
|
618 |
|
619 |
-
// Clear last results
|
620 |
this.lastResults = null;
|
621 |
|
622 |
// Use preprocessed data if available
|
@@ -651,14 +692,21 @@ class ClozeGame {
|
|
651 |
}
|
652 |
|
653 |
nextRound() {
|
654 |
-
// Check if user passed the previous round
|
655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
656 |
|
657 |
// Always increment round counter
|
658 |
this.currentRound++;
|
659 |
|
660 |
-
// Only advance level if user passed
|
661 |
-
if (
|
662 |
this.currentLevel++;
|
663 |
}
|
664 |
// If failed, stay at same level
|
@@ -666,8 +714,9 @@ class ClozeGame {
|
|
666 |
// Clear chat conversations for new round
|
667 |
this.chatService.clearConversations();
|
668 |
|
669 |
-
// Clear
|
670 |
this.lastResults = null;
|
|
|
671 |
|
672 |
return this.startNewRound();
|
673 |
}
|
|
|
19 |
this.hints = [];
|
20 |
this.chatService = new ChatService(aiService);
|
21 |
this.lastResults = null; // Store results for answer revelation
|
22 |
+
this.roundResults = []; // Store results for both passages in current round
|
23 |
|
24 |
// Two-passage system properties
|
25 |
this.currentBooks = []; // Array of two books per round
|
|
|
53 |
this.currentPassageIndex = 0;
|
54 |
|
55 |
// Calculate blanks per passage based on level
|
56 |
+
// Levels 1-5: 1 blank, Levels 6-10: 2 blanks, Level 11+: 3 blanks
|
57 |
let blanksPerPassage;
|
58 |
+
if (this.currentLevel <= 5) {
|
59 |
blanksPerPassage = 1;
|
60 |
+
} else if (this.currentLevel <= 10) {
|
61 |
blanksPerPassage = 2;
|
62 |
} else {
|
63 |
blanksPerPassage = 3;
|
|
|
66 |
// Process both passages in a single API call
|
67 |
try {
|
68 |
const batchResult = await aiService.processBothPassages(
|
69 |
+
passage1, book1, passage2, book2, blanksPerPassage, this.currentLevel
|
70 |
);
|
71 |
|
72 |
// Store the preprocessed data for both passages
|
73 |
this.preprocessedData = batchResult;
|
74 |
|
75 |
+
// Debug: Log what the AI returned
|
76 |
+
console.log(`Level ${this.currentLevel}: Requested ${blanksPerPassage} blanks per passage`);
|
77 |
+
console.log(`Passage 1 received ${batchResult.passage1.words.length} words:`, batchResult.passage1.words);
|
78 |
+
console.log(`Passage 2 received ${batchResult.passage2.words.length} words:`, batchResult.passage2.words);
|
79 |
+
|
80 |
// Set up first passage using preprocessed data
|
81 |
this.currentBook = book1;
|
82 |
this.originalText = this.passages[0];
|
|
|
192 |
async createClozeTextFromPreprocessed(passageIndex) {
|
193 |
// Use preprocessed word selection from batch API call
|
194 |
const preprocessed = passageIndex === 0 ? this.preprocessedData.passage1 : this.preprocessedData.passage2;
|
195 |
+
let selectedWords = preprocessed.words;
|
196 |
+
|
197 |
+
// Calculate expected number of blanks based on level
|
198 |
+
let expectedBlanks;
|
199 |
+
if (this.currentLevel <= 5) {
|
200 |
+
expectedBlanks = 1;
|
201 |
+
} else if (this.currentLevel <= 10) {
|
202 |
+
expectedBlanks = 2;
|
203 |
+
} else {
|
204 |
+
expectedBlanks = 3;
|
205 |
+
}
|
206 |
+
|
207 |
+
// Limit selected words to expected number
|
208 |
+
if (selectedWords.length > expectedBlanks) {
|
209 |
+
console.log(`AI returned ${selectedWords.length} words but expected ${expectedBlanks}, limiting to ${expectedBlanks}`);
|
210 |
+
selectedWords = selectedWords.slice(0, expectedBlanks);
|
211 |
+
}
|
212 |
|
213 |
// Split passage into words
|
214 |
const words = this.originalText.split(/(\s+)/);
|
215 |
const wordsOnly = words.filter(w => w.trim() !== '');
|
216 |
|
217 |
+
// Find indices of selected words using exact matching
|
218 |
const selectedIndices = [];
|
219 |
selectedWords.forEach(word => {
|
220 |
+
// First try exact match (cleaned)
|
221 |
+
let index = wordsOnly.findIndex((w, idx) => {
|
222 |
+
const cleanW = w.replace(/[^\w]/g, '').toLowerCase();
|
223 |
+
const cleanWord = word.replace(/[^\w]/g, '').toLowerCase();
|
224 |
+
return cleanW === cleanWord && !selectedIndices.includes(idx);
|
225 |
+
});
|
226 |
+
|
227 |
+
// Fallback to includes match if exact fails
|
228 |
+
if (index === -1) {
|
229 |
+
index = wordsOnly.findIndex((w, idx) =>
|
230 |
+
w.toLowerCase().includes(word.toLowerCase()) && !selectedIndices.includes(idx)
|
231 |
+
);
|
232 |
+
}
|
233 |
+
|
234 |
if (index !== -1) {
|
235 |
selectedIndices.push(index);
|
236 |
+
} else {
|
237 |
+
console.warn(`Could not find word "${word}" in passage`);
|
238 |
}
|
239 |
});
|
240 |
|
|
|
243 |
this.hints = [];
|
244 |
const clozeWords = [...wordsOnly];
|
245 |
|
246 |
+
console.log(`Creating ${selectedIndices.length} blanks from ${selectedWords.length} selected words`);
|
247 |
+
|
248 |
selectedIndices.forEach((wordIndex, blankIndex) => {
|
249 |
const originalWord = wordsOnly[wordIndex];
|
250 |
const cleanWord = originalWord.replace(/[^\w]/g, '');
|
|
|
296 |
|
297 |
async createClozeText() {
|
298 |
const words = this.originalText.split(' ');
|
299 |
+
// Progressive difficulty: levels 1-5 = 1 blank, levels 6-10 = 2 blanks, level 11+ = 3 blanks
|
300 |
let numberOfBlanks;
|
301 |
+
if (this.currentLevel <= 5) {
|
302 |
numberOfBlanks = 1;
|
303 |
+
} else if (this.currentLevel <= 10) {
|
304 |
numberOfBlanks = 2;
|
305 |
} else {
|
306 |
numberOfBlanks = 3;
|
|
|
314 |
try {
|
315 |
significantWords = await aiService.selectSignificantWords(
|
316 |
this.originalText,
|
317 |
+
numberOfBlanks,
|
318 |
+
this.currentLevel
|
319 |
);
|
320 |
console.log('AI selected words:', significantWords);
|
321 |
} catch (error) {
|
|
|
618 |
|
619 |
// Store results for potential answer revelation
|
620 |
this.lastResults = resultsData;
|
621 |
+
|
622 |
+
// Store results for round-level tracking
|
623 |
+
this.roundResults[this.currentPassageIndex] = resultsData;
|
624 |
|
625 |
return resultsData;
|
626 |
}
|
|
|
657 |
// Clear chat conversations for new passage
|
658 |
this.chatService.clearConversations();
|
659 |
|
660 |
+
// Clear last results (but keep roundResults for level advancement)
|
661 |
this.lastResults = null;
|
662 |
|
663 |
// Use preprocessed data if available
|
|
|
692 |
}
|
693 |
|
694 |
nextRound() {
|
695 |
+
// Check if user passed the previous round based on overall round performance
|
696 |
+
let roundPassed = false;
|
697 |
+
if (this.roundResults.length === 2) {
|
698 |
+
// Both passages completed - check if user passed at least one passage
|
699 |
+
roundPassed = this.roundResults.some(result => result && result.passed);
|
700 |
+
} else if (this.lastResults) {
|
701 |
+
// Fallback to single passage result
|
702 |
+
roundPassed = this.lastResults.passed;
|
703 |
+
}
|
704 |
|
705 |
// Always increment round counter
|
706 |
this.currentRound++;
|
707 |
|
708 |
+
// Only advance level if user passed the round
|
709 |
+
if (roundPassed) {
|
710 |
this.currentLevel++;
|
711 |
}
|
712 |
// If failed, stay at same level
|
|
|
714 |
// Clear chat conversations for new round
|
715 |
this.chatService.clearConversations();
|
716 |
|
717 |
+
// Clear results since we're moving to new round
|
718 |
this.lastResults = null;
|
719 |
+
this.roundResults = [];
|
720 |
|
721 |
return this.startNewRound();
|
722 |
}
|
src/init-env.js
CHANGED
@@ -13,7 +13,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
13 |
if (hfMeta && hfMeta.content) {
|
14 |
window.HF_API_KEY = hfMeta.content;
|
15 |
console.log('HF API key loaded');
|
16 |
-
} else {
|
17 |
-
console.log('No HF API key found in meta tags');
|
18 |
}
|
19 |
});
|
|
|
13 |
if (hfMeta && hfMeta.content) {
|
14 |
window.HF_API_KEY = hfMeta.content;
|
15 |
console.log('HF API key loaded');
|
|
|
|
|
16 |
}
|
17 |
});
|