Spaces:
Sleeping
Sleeping
feat: implement two-passage system to avoid OpenRouter rate limits
Browse files- Each round now loads two different passages from two books
- Added passage numbering (Passage 1/2, Passage 2/2)
- Modified nextPassage() method to handle second passage in same round
- Updated UI to show current passage number
- Added proper error handling for OpenRouter API errors
- This spreads out API calls to avoid rate limiting issues
- src/aiService.js +10 -4
- src/app.js +15 -5
- src/clozeGameEngine.js +62 -7
src/aiService.js
CHANGED
|
@@ -139,8 +139,11 @@ Passage: "${passage}"`
|
|
| 139 |
|
| 140 |
const data = await response.json();
|
| 141 |
|
| 142 |
-
//
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
// Check if response has expected structure
|
| 146 |
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
|
@@ -218,8 +221,11 @@ Passage: "${passage}"`
|
|
| 218 |
|
| 219 |
const data = await response.json();
|
| 220 |
|
| 221 |
-
//
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
// Check if response has expected structure
|
| 225 |
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
|
|
|
| 139 |
|
| 140 |
const data = await response.json();
|
| 141 |
|
| 142 |
+
// Check for OpenRouter error response
|
| 143 |
+
if (data.error) {
|
| 144 |
+
console.error('OpenRouter API error for word selection:', data.error);
|
| 145 |
+
throw new Error(`OpenRouter API error: ${data.error.message || JSON.stringify(data.error)}`);
|
| 146 |
+
}
|
| 147 |
|
| 148 |
// Check if response has expected structure
|
| 149 |
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
|
|
|
| 221 |
|
| 222 |
const data = await response.json();
|
| 223 |
|
| 224 |
+
// Check for OpenRouter error response
|
| 225 |
+
if (data.error) {
|
| 226 |
+
console.error('OpenRouter API error for contextualization:', data.error);
|
| 227 |
+
throw new Error(`OpenRouter API error: ${data.error.message || JSON.stringify(data.error)}`);
|
| 228 |
+
}
|
| 229 |
|
| 230 |
// Check if response has expected structure
|
| 231 |
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
src/app.js
CHANGED
|
@@ -69,10 +69,11 @@ class App {
|
|
| 69 |
<strong>${roundData.title}</strong> by ${roundData.author}
|
| 70 |
`;
|
| 71 |
|
| 72 |
-
// Show level information
|
| 73 |
const blanksCount = roundData.blanks.length;
|
| 74 |
const difficultyText = blanksCount === 1 ? 'Easy' : blanksCount === 2 ? 'Medium' : 'Hard';
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
// Show contextualization from AI agent
|
| 78 |
this.elements.contextualization.innerHTML = `
|
|
@@ -206,15 +207,24 @@ class App {
|
|
| 206 |
// Show loading immediately with specific message
|
| 207 |
this.showLoading(true, 'Loading next passage...');
|
| 208 |
|
| 209 |
-
// Clear chat history when starting new round
|
| 210 |
this.chatUI.clearChatHistory();
|
| 211 |
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
this.displayRound(roundData);
|
| 214 |
this.resetUI();
|
| 215 |
this.showLoading(false);
|
| 216 |
} catch (error) {
|
| 217 |
-
console.error('Error loading next
|
| 218 |
this.showError('Could not load next passage. Please try again.');
|
| 219 |
}
|
| 220 |
}
|
|
|
|
| 69 |
<strong>${roundData.title}</strong> by ${roundData.author}
|
| 70 |
`;
|
| 71 |
|
| 72 |
+
// Show level information with passage number
|
| 73 |
const blanksCount = roundData.blanks.length;
|
| 74 |
const difficultyText = blanksCount === 1 ? 'Easy' : blanksCount === 2 ? 'Medium' : 'Hard';
|
| 75 |
+
const passageInfo = roundData.passageNumber ? `Passage ${roundData.passageNumber}/${roundData.totalPassages} • ` : '';
|
| 76 |
+
this.elements.roundInfo.innerHTML = `Level ${this.game.currentLevel} • ${passageInfo}${blanksCount} blank${blanksCount > 1 ? 's' : ''} • ${difficultyText}`;
|
| 77 |
|
| 78 |
// Show contextualization from AI agent
|
| 79 |
this.elements.contextualization.innerHTML = `
|
|
|
|
| 207 |
// Show loading immediately with specific message
|
| 208 |
this.showLoading(true, 'Loading next passage...');
|
| 209 |
|
| 210 |
+
// Clear chat history when starting new passage/round
|
| 211 |
this.chatUI.clearChatHistory();
|
| 212 |
|
| 213 |
+
// Check if we should load next passage or next round
|
| 214 |
+
let roundData;
|
| 215 |
+
if (this.game.currentPassageIndex === 0) {
|
| 216 |
+
// Load second passage in current round
|
| 217 |
+
roundData = await this.game.nextPassage();
|
| 218 |
+
} else {
|
| 219 |
+
// Load next round (two new passages)
|
| 220 |
+
roundData = await this.game.nextRound();
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
this.displayRound(roundData);
|
| 224 |
this.resetUI();
|
| 225 |
this.showLoading(false);
|
| 226 |
} catch (error) {
|
| 227 |
+
console.error('Error loading next passage:', error);
|
| 228 |
this.showError('Could not load next passage. Please try again.');
|
| 229 |
}
|
| 230 |
}
|
src/clozeGameEngine.js
CHANGED
|
@@ -19,6 +19,11 @@ class ClozeGame {
|
|
| 19 |
this.hints = [];
|
| 20 |
this.chatService = new ChatService(aiService);
|
| 21 |
this.lastResults = null; // Store results for answer revelation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
async initialize() {
|
|
@@ -33,14 +38,22 @@ class ClozeGame {
|
|
| 33 |
|
| 34 |
async startNewRound() {
|
| 35 |
try {
|
| 36 |
-
// Get
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
//
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
|
| 45 |
// Run AI calls in parallel for faster loading
|
| 46 |
const [clozeResult, contextualizationResult] = await Promise.all([
|
|
@@ -54,7 +67,9 @@ class ClozeGame {
|
|
| 54 |
text: this.clozeText,
|
| 55 |
blanks: this.blanks,
|
| 56 |
contextualization: this.contextualization,
|
| 57 |
-
hints: this.hints
|
|
|
|
|
|
|
| 58 |
};
|
| 59 |
} catch (error) {
|
| 60 |
console.error('Error starting new round:', error);
|
|
@@ -451,6 +466,46 @@ class ClozeGame {
|
|
| 451 |
}));
|
| 452 |
}
|
| 453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
nextRound() {
|
| 455 |
// Check if user passed the previous round
|
| 456 |
const passed = this.lastResults && this.lastResults.passed;
|
|
|
|
| 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
|
| 25 |
+
this.passages = []; // Array of two passages per round
|
| 26 |
+
this.currentPassageIndex = 0; // 0 for first passage, 1 for second
|
| 27 |
}
|
| 28 |
|
| 29 |
async initialize() {
|
|
|
|
| 38 |
|
| 39 |
async startNewRound() {
|
| 40 |
try {
|
| 41 |
+
// Get two random books for this round
|
| 42 |
+
const book1 = await bookDataService.getRandomBook();
|
| 43 |
+
const book2 = await bookDataService.getRandomBook();
|
| 44 |
+
|
| 45 |
+
// Extract passages from both books
|
| 46 |
+
const passage1 = this.extractCoherentPassage(book1.text);
|
| 47 |
+
const passage2 = this.extractCoherentPassage(book2.text);
|
| 48 |
|
| 49 |
+
// Store both books and passages
|
| 50 |
+
this.currentBooks = [book1, book2];
|
| 51 |
+
this.passages = [passage1.trim(), passage2.trim()];
|
| 52 |
+
this.currentPassageIndex = 0;
|
| 53 |
|
| 54 |
+
// Start with the first passage
|
| 55 |
+
this.currentBook = book1;
|
| 56 |
+
this.originalText = this.passages[0];
|
| 57 |
|
| 58 |
// Run AI calls in parallel for faster loading
|
| 59 |
const [clozeResult, contextualizationResult] = await Promise.all([
|
|
|
|
| 67 |
text: this.clozeText,
|
| 68 |
blanks: this.blanks,
|
| 69 |
contextualization: this.contextualization,
|
| 70 |
+
hints: this.hints,
|
| 71 |
+
passageNumber: 1,
|
| 72 |
+
totalPassages: 2
|
| 73 |
};
|
| 74 |
} catch (error) {
|
| 75 |
console.error('Error starting new round:', error);
|
|
|
|
| 466 |
}));
|
| 467 |
}
|
| 468 |
|
| 469 |
+
async nextPassage() {
|
| 470 |
+
try {
|
| 471 |
+
// Move to the second passage in the current round
|
| 472 |
+
if (this.currentPassageIndex === 0 && this.passages && this.passages.length > 1) {
|
| 473 |
+
this.currentPassageIndex = 1;
|
| 474 |
+
this.currentBook = this.currentBooks[1];
|
| 475 |
+
this.originalText = this.passages[1];
|
| 476 |
+
|
| 477 |
+
// Clear chat conversations for new passage
|
| 478 |
+
this.chatService.clearConversations();
|
| 479 |
+
|
| 480 |
+
// Clear last results
|
| 481 |
+
this.lastResults = null;
|
| 482 |
+
|
| 483 |
+
// Generate new cloze text and contextualization for second passage
|
| 484 |
+
const [clozeResult, contextualizationResult] = await Promise.all([
|
| 485 |
+
this.createClozeText(),
|
| 486 |
+
this.generateContextualization()
|
| 487 |
+
]);
|
| 488 |
+
|
| 489 |
+
return {
|
| 490 |
+
title: this.currentBook.title,
|
| 491 |
+
author: this.currentBook.author,
|
| 492 |
+
text: this.clozeText,
|
| 493 |
+
blanks: this.blanks,
|
| 494 |
+
contextualization: this.contextualization,
|
| 495 |
+
hints: this.hints,
|
| 496 |
+
passageNumber: 2,
|
| 497 |
+
totalPassages: 2
|
| 498 |
+
};
|
| 499 |
+
} else {
|
| 500 |
+
// If we're already on the second passage, move to next round
|
| 501 |
+
return this.nextRound();
|
| 502 |
+
}
|
| 503 |
+
} catch (error) {
|
| 504 |
+
console.error('Error loading next passage:', error);
|
| 505 |
+
throw error;
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
nextRound() {
|
| 510 |
// Check if user passed the previous round
|
| 511 |
const passed = this.lastResults && this.lastResults.passed;
|