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 |
});
|