milwright commited on
Commit
cd16e7b
·
1 Parent(s): b37374a

feat: implement batch API processing for both passages

Browse files

- Add processBothPassages method to handle both passages in single API call
- Preprocess word selection and contextualization for both passages upfront
- Reduce API calls from 4 to 1 per round to avoid rate limits
- Add fallback to sequential processing if batch fails
- Improve user experience with faster passage loading

Files changed (2) hide show
  1. src/aiService.js +69 -0
  2. src/clozeGameEngine.js +96 -17
src/aiService.js CHANGED
@@ -183,6 +183,75 @@ Passage: "${passage}"`
183
  }
184
  }
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  async generateContextualization(title, author) {
187
  console.log('generateContextualization called for:', title, 'by', author);
188
 
 
183
  }
184
  }
185
 
186
+ async processBothPassages(passage1, book1, passage2, book2, blanksPerPassage) {
187
+ // Process both passages in a single API call to avoid rate limits
188
+ const currentKey = this.getApiKey();
189
+ if (currentKey && !this.apiKey) {
190
+ this.apiKey = currentKey;
191
+ }
192
+
193
+ if (!this.apiKey) {
194
+ throw new Error('API key required for passage processing');
195
+ }
196
+
197
+ try {
198
+ const response = await fetch(this.apiUrl, {
199
+ method: 'POST',
200
+ headers: {
201
+ 'Content-Type': 'application/json',
202
+ 'Authorization': `Bearer ${this.apiKey}`,
203
+ 'HTTP-Referer': window.location.origin,
204
+ 'X-Title': 'Cloze Reader'
205
+ },
206
+ body: JSON.stringify({
207
+ model: this.model,
208
+ messages: [{
209
+ role: 'system',
210
+ content: 'Process two passages for a cloze reading exercise. For each passage: 1) Select words for blanks, 2) Generate a contextual introduction. Return a JSON object with both passages\' data.'
211
+ }, {
212
+ role: 'user',
213
+ content: `Process these two passages for cloze exercises:
214
+
215
+ PASSAGE 1:
216
+ Title: "${book1.title}" by ${book1.author}
217
+ Text: "${passage1}"
218
+ Select ${blanksPerPassage} words for blanks.
219
+
220
+ PASSAGE 2:
221
+ Title: "${book2.title}" by ${book2.author}
222
+ Text: "${passage2}"
223
+ Select ${blanksPerPassage} words for blanks.
224
+
225
+ For each passage return:
226
+ - "words": array of selected words (exactly as they appear)
227
+ - "context": one-sentence intro about the book/author
228
+
229
+ Return as JSON: {"passage1": {...}, "passage2": {...}}`
230
+ }],
231
+ max_tokens: 500,
232
+ temperature: 0.3
233
+ })
234
+ });
235
+
236
+ if (!response.ok) {
237
+ throw new Error(`API request failed: ${response.status}`);
238
+ }
239
+
240
+ const data = await response.json();
241
+ const content = data.choices[0].message.content.trim();
242
+
243
+ try {
244
+ return JSON.parse(content);
245
+ } catch (e) {
246
+ console.error('Failed to parse batch response:', e);
247
+ throw new Error('Invalid API response format');
248
+ }
249
+ } catch (error) {
250
+ console.error('Error processing passages:', error);
251
+ throw error;
252
+ }
253
+ }
254
+
255
  async generateContextualization(title, author) {
256
  console.log('generateContextualization called for:', title, 'by', author);
257
 
src/clozeGameEngine.js CHANGED
@@ -51,17 +51,33 @@ class ClozeGame {
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 sequentially to avoid rate limiting
59
- await this.createClozeText();
60
-
61
- // Add small delay between API calls to avoid rate limiting
62
- await new Promise(resolve => setTimeout(resolve, 1000));
63
-
64
- await this.generateContextualization();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  return {
67
  title: this.currentBook.title,
@@ -159,6 +175,66 @@ class ClozeGame {
159
  return passage.trim();
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  async createClozeText() {
163
  const words = this.originalText.split(' ');
164
  // Progressive difficulty: levels 1-2 = 1 blank, levels 3-4 = 2 blanks, level 5+ = 3 blanks
@@ -511,13 +587,16 @@ class ClozeGame {
511
  // Clear last results
512
  this.lastResults = null;
513
 
514
- // Generate new cloze text and contextualization for second passage sequentially
515
- await this.createClozeText();
516
-
517
- // Add small delay between API calls to avoid rate limiting
518
- await new Promise(resolve => setTimeout(resolve, 1000));
519
-
520
- await this.generateContextualization();
 
 
 
521
 
522
  return {
523
  title: this.currentBook.title,
 
51
  this.passages = [passage1.trim(), passage2.trim()];
52
  this.currentPassageIndex = 0;
53
 
54
+ // Calculate blanks per passage based on level
55
+ const blanksPerPassage = Math.min(this.currentLevel + 2, 5);
 
56
 
57
+ // Process both passages in a single API call
58
+ try {
59
+ const batchResult = await aiService.processBothPassages(
60
+ passage1, book1, passage2, book2, blanksPerPassage
61
+ );
62
+
63
+ // Store the preprocessed data for both passages
64
+ this.preprocessedData = batchResult;
65
+
66
+ // Set up first passage using preprocessed data
67
+ this.currentBook = book1;
68
+ this.originalText = this.passages[0];
69
+ await this.createClozeTextFromPreprocessed(0);
70
+ this.contextualization = this.preprocessedData.passage1.context;
71
+
72
+ } catch (error) {
73
+ console.warn('Batch processing failed, falling back to sequential:', error);
74
+ // Fallback to sequential processing
75
+ this.currentBook = book1;
76
+ this.originalText = this.passages[0];
77
+ await this.createClozeText();
78
+ await new Promise(resolve => setTimeout(resolve, 1000));
79
+ await this.generateContextualization();
80
+ }
81
 
82
  return {
83
  title: this.currentBook.title,
 
175
  return passage.trim();
176
  }
177
 
178
+ async createClozeTextFromPreprocessed(passageIndex) {
179
+ // Use preprocessed word selection from batch API call
180
+ const preprocessed = passageIndex === 0 ? this.preprocessedData.passage1 : this.preprocessedData.passage2;
181
+ const selectedWords = preprocessed.words;
182
+
183
+ // Split passage into words
184
+ const words = this.originalText.split(/(\s+)/);
185
+ const wordsOnly = words.filter(w => w.trim() !== '');
186
+
187
+ // Find indices of selected words
188
+ const selectedIndices = [];
189
+ selectedWords.forEach(word => {
190
+ const index = wordsOnly.findIndex((w, idx) =>
191
+ w.toLowerCase().includes(word.toLowerCase()) && !selectedIndices.includes(idx)
192
+ );
193
+ if (index !== -1) {
194
+ selectedIndices.push(index);
195
+ }
196
+ });
197
+
198
+ // Create blanks
199
+ this.blanks = [];
200
+ this.hints = [];
201
+ const clozeWords = [...wordsOnly];
202
+
203
+ selectedIndices.forEach((wordIndex, blankIndex) => {
204
+ const originalWord = wordsOnly[wordIndex];
205
+ const cleanWord = originalWord.replace(/[^\w]/g, '');
206
+
207
+ this.blanks.push({
208
+ index: blankIndex,
209
+ originalWord: cleanWord,
210
+ wordIndex: wordIndex
211
+ });
212
+
213
+ // Generate structural hint
214
+ const hint = this.currentLevel <= 2
215
+ ? `${cleanWord.length} letters, starts with "${cleanWord[0]}", ends with "${cleanWord[cleanWord.length - 1]}"`
216
+ : `${cleanWord.length} letters, starts with "${cleanWord[0]}"`;
217
+ this.hints.push({ index: blankIndex, hint });
218
+
219
+ // Replace with placeholder
220
+ clozeWords[wordIndex] = `___BLANK_${blankIndex}___`;
221
+ });
222
+
223
+ // Reconstruct text with original spacing
224
+ let reconstructed = '';
225
+ let wordIndex = 0;
226
+ words.forEach(part => {
227
+ if (part.trim() === '') {
228
+ reconstructed += part;
229
+ } else {
230
+ reconstructed += clozeWords[wordIndex++];
231
+ }
232
+ });
233
+
234
+ this.clozeText = reconstructed;
235
+ this.userAnswers = new Array(this.blanks.length).fill('');
236
+ }
237
+
238
  async createClozeText() {
239
  const words = this.originalText.split(' ');
240
  // Progressive difficulty: levels 1-2 = 1 blank, levels 3-4 = 2 blanks, level 5+ = 3 blanks
 
587
  // Clear last results
588
  this.lastResults = null;
589
 
590
+ // Use preprocessed data if available
591
+ if (this.preprocessedData && this.preprocessedData.passage2) {
592
+ await this.createClozeTextFromPreprocessed(1);
593
+ this.contextualization = this.preprocessedData.passage2.context;
594
+ } else {
595
+ // Fallback to sequential processing
596
+ await this.createClozeText();
597
+ await new Promise(resolve => setTimeout(resolve, 1000));
598
+ await this.generateContextualization();
599
+ }
600
 
601
  return {
602
  title: this.currentBook.title,