amauricunha commited on
Commit
78727e0
·
verified ·
1 Parent(s): 2e62f01

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +143 -47
index.html CHANGED
@@ -37,6 +37,14 @@
37
  .btn-secondary:hover {
38
  background-color: #d1d5db;
39
  }
 
 
 
 
 
 
 
 
40
  /* Style for the Content Editable Editor */
41
  #textEditor {
42
  border: 1px solid #ccc;
@@ -95,6 +103,35 @@
95
  color: white;
96
  border-bottom: 2px solid #4f46e5;
97
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  /* Loading Style (Spinner) */
99
  .spinner {
100
  border: 4px solid rgba(0, 0, 0, 0.1);
@@ -130,6 +167,12 @@
130
  class="w-full p-4 focus:ring-indigo-500 focus:border-indigo-500 text-gray-800"></div>
131
 
132
  <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/5000</p>
 
 
 
 
 
 
133
  </div>
134
 
135
  <div class="card mb-6">
@@ -178,6 +221,15 @@
178
  </div>
179
  </div>
180
  </div>
 
 
 
 
 
 
 
 
 
181
  </div>
182
 
183
  <!-- Translation and Context Modal -->
@@ -216,7 +268,7 @@
216
 
217
  <script>
218
  // Global variables
219
- const textEditor = document.getElementById('textEditor'); // Renamed from textInput
220
  const playButton = document.getElementById('playButton');
221
  const audioPlayer = document.getElementById('audioPlayer');
222
  const charCount = document.getElementById('charCount');
@@ -224,6 +276,9 @@
224
  const playText = document.getElementById('playText');
225
  const loopSelectionButton = document.getElementById('loopSelectionButton');
226
  const clearLoopButton = document.getElementById('clearLoopButton');
 
 
 
227
  const speedSelector = document.getElementById('speedSelector');
228
 
229
  const MAX_CHARS = 5000;
@@ -234,7 +289,6 @@
234
  // --- Interface and Editor Management ---
235
 
236
  function getEditorText() {
237
- // Get text content, not HTML, to ensure clean input for TTS and AI
238
  return textEditor.innerText.trim();
239
  }
240
 
@@ -263,14 +317,6 @@
263
  textEditor.addEventListener('input', () => {
264
  const currentLength = getEditorText().length;
265
  charCount.textContent = currentLength;
266
-
267
- // Limit input to max characters
268
- if (currentLength > MAX_CHARS) {
269
- // Warning: This truncation is crude for contenteditable, user must manually delete
270
- // Since this is study tool, we rely on the user to manage content length.
271
- // Truncation logic is best left for the server, but we warn here.
272
- }
273
- // Enable button if there is text
274
  playButton.disabled = currentLength === 0;
275
  });
276
 
@@ -281,6 +327,13 @@
281
  audioPlayer.playbackRate = parseFloat(speedSelector.value);
282
  });
283
 
 
 
 
 
 
 
 
284
  // --- Main Audio and Loop Functionality ---
285
 
286
  async function fetchAudio(text) {
@@ -300,10 +353,8 @@
300
  throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
301
  }
302
 
303
- // The backend returns an audio blob (MP3)
304
  const audioBlob = await response.blob();
305
 
306
- // Revoke previous URL to free up memory
307
  if (lastAudioUrl) URL.revokeObjectURL(lastAudioUrl);
308
 
309
  lastAudioUrl = URL.createObjectURL(audioBlob);
@@ -330,7 +381,6 @@
330
  }
331
  }
332
 
333
- // Sets the selection for repetition (Phrase Loop)
334
  function setLoopMode() {
335
  const selection = window.getSelection().toString().trim();
336
  if (selection) {
@@ -350,7 +400,6 @@
350
  clearLoopButton.disabled = true;
351
  audioPlayer.loop = false;
352
  audioPlayer.title = '';
353
- // Return to main audio (if available)
354
  if (getEditorText()) {
355
  handlePlay();
356
  }
@@ -358,71 +407,68 @@
358
 
359
  audioPlayer.addEventListener('ended', () => {
360
  if (isLooping) {
361
- // If in loop mode, restart playback
362
  audioPlayer.currentTime = 0;
363
  audioPlayer.play();
364
  }
365
  });
366
 
367
- // Enable Loop button if there is a selection
368
- textEditor.addEventListener('mouseup', () => {
369
- const selection = window.getSelection().toString().trim();
370
- loopSelectionButton.disabled = !selection;
371
- });
372
-
373
-
374
  // --- Quick Translation Functionality (Double Click) ---
375
 
376
- // Activate the AI API call for translation
377
- async function fetchExplanationAndTranslation(word, context) {
378
- const explanationSpinner = document.getElementById('explanationSpinner');
379
- const translationSpinner = document.getElementById('translationSpinner');
380
- const explanationText = document.getElementById('explanationText');
381
- const translationText = document.getElementById('translationText');
382
-
383
- explanationText.textContent = '';
384
- translationText.textContent = '';
385
- explanationSpinner.classList.remove('hidden');
386
- translationSpinner.classList.remove('hidden');
387
 
388
  try {
389
- // Route for the backend using Gemini for explanation/translation
390
  const response = await fetch('/explain-proxy', {
391
  method: 'POST',
392
  headers: { 'Content-Type': 'application/json' },
393
- // Sends the clicked word and context to the AI
394
- body: JSON.stringify({ word: word, context: context })
395
  });
396
 
397
  if (!response.ok) {
398
- throw new Error(`HTTP Error: ${response.status}. Check if GEMINI_API_KEY is configured in your Space.`);
 
399
  }
400
 
401
  const data = await response.json();
402
 
403
- // Assumes the backend returns an object { explanation: "...", translation: "..." }
404
- explanationText.textContent = data.explanation || 'Could not retrieve explanation.';
405
- translationText.textContent = data.translation || 'Could not retrieve translation.';
 
 
 
 
406
 
407
  } catch (error) {
408
  console.error('Error fetching context from AI:', error);
409
- explanationText.textContent = 'Error connecting to AI. Check if GEMINI_API_KEY is configured correctly.';
410
- translationText.textContent = 'Error connecting to AI.';
 
 
 
 
 
 
411
  } finally {
412
- explanationSpinner.classList.add('hidden');
413
- translationSpinner.classList.add('hidden');
414
  }
415
  }
416
 
417
  // Logic to open the Modal on double click
418
  textEditor.addEventListener('dblclick', (event) => {
419
- // Check if the click occurred on a text node or an element inside the editor
420
  const selectedWord = window.getSelection().toString().trim().replace(/[^a-zA-Z']/g, '');
421
 
422
  if (selectedWord) {
423
  const fullText = getEditorText();
424
  const index = fullText.indexOf(selectedWord);
425
- // Capture context (a few words before and after)
426
  const context = fullText.substring(Math.max(0, index - 50), index + selectedWord.length + 50);
427
 
428
  document.getElementById('wordSelected').textContent = selectedWord;
@@ -433,6 +479,57 @@
433
  }
434
  });
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  // --- Modal Control ---
437
 
438
  const modal = document.getElementById('explanationModal');
@@ -441,7 +538,6 @@
441
 
442
  function openModal() {
443
  modal.style.display = 'block';
444
- // Ensure Explanation tab is the first one shown
445
  showTab('explanation');
446
  }
447
 
 
37
  .btn-secondary:hover {
38
  background-color: #d1d5db;
39
  }
40
+ .btn-success {
41
+ background-color: #10b981;
42
+ color: white;
43
+ transition: background-color 0.2s;
44
+ }
45
+ .btn-success:hover {
46
+ background-color: #059669;
47
+ }
48
  /* Style for the Content Editable Editor */
49
  #textEditor {
50
  border: 1px solid #ccc;
 
103
  color: white;
104
  border-bottom: 2px solid #4f46e5;
105
  }
106
+ /* Flashcard style */
107
+ .flashcard {
108
+ background-color: #f3f4f6;
109
+ padding: 15px;
110
+ border-radius: 8px;
111
+ margin-bottom: 10px;
112
+ cursor: pointer;
113
+ transition: all 0.2s;
114
+ border: 1px solid #e5e7eb;
115
+ }
116
+ .flashcard:hover {
117
+ background-color: #e5e7eb;
118
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
119
+ }
120
+ .flashcard-term {
121
+ font-weight: 600;
122
+ color: #4f46e5;
123
+ text-decoration: underline;
124
+ }
125
+ .flashcard-definition {
126
+ font-size: 0.9em;
127
+ color: #4b5563;
128
+ }
129
+ .flashcard-example {
130
+ font-style: italic;
131
+ font-size: 0.8em;
132
+ color: #6b7280;
133
+ margin-top: 5px;
134
+ }
135
  /* Loading Style (Spinner) */
136
  .spinner {
137
  border: 4px solid rgba(0, 0, 0, 0.1);
 
167
  class="w-full p-4 focus:ring-indigo-500 focus:border-indigo-500 text-gray-800"></div>
168
 
169
  <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/5000</p>
170
+
171
+ <div class="mt-4 border-t pt-4">
172
+ <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn-success px-4 py-2 rounded-lg font-medium disabled:opacity-50" disabled>
173
+ + Create Flashcard from Selection
174
+ </button>
175
+ </div>
176
  </div>
177
 
178
  <div class="card mb-6">
 
221
  </div>
222
  </div>
223
  </div>
224
+
225
+ <!-- New Flashcards Panel -->
226
+ <div id="flashcardsPanel" class="card">
227
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">Your Flashcards (Study Session)</h2>
228
+ <div id="flashcardList" class="space-y-3">
229
+ <p id="noCardsMessage" class="text-gray-500 italic">No flashcards created yet. Select a word and click 'Create Flashcard'.</p>
230
+ </div>
231
+ </div>
232
+
233
  </div>
234
 
235
  <!-- Translation and Context Modal -->
 
268
 
269
  <script>
270
  // Global variables
271
+ const textEditor = document.getElementById('textEditor');
272
  const playButton = document.getElementById('playButton');
273
  const audioPlayer = document.getElementById('audioPlayer');
274
  const charCount = document.getElementById('charCount');
 
276
  const playText = document.getElementById('playText');
277
  const loopSelectionButton = document.getElementById('loopSelectionButton');
278
  const clearLoopButton = document.getElementById('clearLoopButton');
279
+ const createCardButton = document.getElementById('createCardButton');
280
+ const flashcardList = document.getElementById('flashcardList');
281
+ const noCardsMessage = document.getElementById('noCardsMessage');
282
  const speedSelector = document.getElementById('speedSelector');
283
 
284
  const MAX_CHARS = 5000;
 
289
  // --- Interface and Editor Management ---
290
 
291
  function getEditorText() {
 
292
  return textEditor.innerText.trim();
293
  }
294
 
 
317
  textEditor.addEventListener('input', () => {
318
  const currentLength = getEditorText().length;
319
  charCount.textContent = currentLength;
 
 
 
 
 
 
 
 
320
  playButton.disabled = currentLength === 0;
321
  });
322
 
 
327
  audioPlayer.playbackRate = parseFloat(speedSelector.value);
328
  });
329
 
330
+ // Enable Loop and Card button if there is a selection
331
+ textEditor.addEventListener('mouseup', () => {
332
+ const selection = window.getSelection().toString().trim();
333
+ loopSelectionButton.disabled = !selection;
334
+ createCardButton.disabled = !selection;
335
+ });
336
+
337
  // --- Main Audio and Loop Functionality ---
338
 
339
  async function fetchAudio(text) {
 
353
  throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
354
  }
355
 
 
356
  const audioBlob = await response.blob();
357
 
 
358
  if (lastAudioUrl) URL.revokeObjectURL(lastAudioUrl);
359
 
360
  lastAudioUrl = URL.createObjectURL(audioBlob);
 
381
  }
382
  }
383
 
 
384
  function setLoopMode() {
385
  const selection = window.getSelection().toString().trim();
386
  if (selection) {
 
400
  clearLoopButton.disabled = true;
401
  audioPlayer.loop = false;
402
  audioPlayer.title = '';
 
403
  if (getEditorText()) {
404
  handlePlay();
405
  }
 
407
 
408
  audioPlayer.addEventListener('ended', () => {
409
  if (isLooping) {
 
410
  audioPlayer.currentTime = 0;
411
  audioPlayer.play();
412
  }
413
  });
414
 
 
 
 
 
 
 
 
415
  // --- Quick Translation Functionality (Double Click) ---
416
 
417
+ async function fetchExplanationAndTranslation(word, context, isFlashcard=false) {
418
+ const spinner = isFlashcard ? document.getElementById('cardSpinner') : document.getElementById('explanationSpinner');
419
+ const explanationText = isFlashcard ? null : document.getElementById('explanationText');
420
+ const translationText = isFlashcard ? null : document.getElementById('translationText');
421
+
422
+ if (!isFlashcard) {
423
+ explanationText.textContent = '';
424
+ translationText.textContent = '';
425
+ }
426
+ spinner.classList.remove('hidden');
 
427
 
428
  try {
 
429
  const response = await fetch('/explain-proxy', {
430
  method: 'POST',
431
  headers: { 'Content-Type': 'application/json' },
432
+ body: JSON.stringify({ word: word, context: context, for_flashcard: isFlashcard })
 
433
  });
434
 
435
  if (!response.ok) {
436
+ const errorMsg = `HTTP Error: ${response.status}. Check if GEMINI_API_KEY is configured in your Space.`;
437
+ throw new Error(errorMsg);
438
  }
439
 
440
  const data = await response.json();
441
 
442
+ if (isFlashcard) {
443
+ return data; // Return full data for flashcard creation
444
+ } else {
445
+ explanationText.textContent = data.explanation || 'Could not retrieve explanation.';
446
+ translationText.textContent = data.translation || 'Could not retrieve translation.';
447
+ }
448
+ return data;
449
 
450
  } catch (error) {
451
  console.error('Error fetching context from AI:', error);
452
+ const msg = 'Error connecting to AI. Check if GEMINI_API_KEY is configured correctly.';
453
+ if (!isFlashcard) {
454
+ explanationText.textContent = msg;
455
+ translationText.textContent = msg;
456
+ } else {
457
+ alert(msg);
458
+ }
459
+ return null;
460
  } finally {
461
+ spinner.classList.add('hidden');
 
462
  }
463
  }
464
 
465
  // Logic to open the Modal on double click
466
  textEditor.addEventListener('dblclick', (event) => {
 
467
  const selectedWord = window.getSelection().toString().trim().replace(/[^a-zA-Z']/g, '');
468
 
469
  if (selectedWord) {
470
  const fullText = getEditorText();
471
  const index = fullText.indexOf(selectedWord);
 
472
  const context = fullText.substring(Math.max(0, index - 50), index + selectedWord.length + 50);
473
 
474
  document.getElementById('wordSelected').textContent = selectedWord;
 
479
  }
480
  });
481
 
482
+ // --- Flashcard Creation Functionality ---
483
+
484
+ async function createFlashcardFromSelection() {
485
+ const selectedText = window.getSelection().toString().trim();
486
+ if (!selectedText) {
487
+ return;
488
+ }
489
+
490
+ const fullText = getEditorText();
491
+ const index = fullText.indexOf(selectedText);
492
+ const context = fullText.substring(Math.max(0, index - 50), index + selectedText.length + 50);
493
+
494
+ createCardButton.disabled = true;
495
+ createCardButton.innerHTML = `<div class="spinner mr-2"></div> Creating Card...`;
496
+
497
+ try {
498
+ // Fetch the structured data for the flashcard
499
+ const cardData = await fetchExplanationAndTranslation(selectedText, context, true);
500
+
501
+ if (cardData && cardData.term && cardData.translation && cardData.example) {
502
+ renderFlashcard(cardData);
503
+ } else {
504
+ alert("The AI could not generate the structured card data. Please try another word or check the API key.");
505
+ }
506
+
507
+ } finally {
508
+ createCardButton.disabled = false;
509
+ createCardButton.innerHTML = `+ Create Flashcard from Selection`;
510
+ }
511
+ }
512
+
513
+ function renderFlashcard(data) {
514
+ noCardsMessage.classList.add('hidden');
515
+
516
+ const cardElement = document.createElement('div');
517
+ cardElement.className = 'flashcard';
518
+
519
+ // Front (Term and Translation)
520
+ cardElement.innerHTML = `
521
+ <div class="flashcard-front">
522
+ <p class="flashcard-term">${data.term.toUpperCase()}</p>
523
+ <p class="flashcard-definition">PT: ${data.translation}</p>
524
+ <p class="flashcard-example">Example: ${data.example}</p>
525
+ </div>
526
+ <!-- Back side (Definition) can be toggled via JS if needed -->
527
+ `;
528
+
529
+ flashcardList.prepend(cardElement); // Add new cards to the top
530
+ }
531
+
532
+
533
  // --- Modal Control ---
534
 
535
  const modal = document.getElementById('explanationModal');
 
538
 
539
  function openModal() {
540
  modal.style.display = 'block';
 
541
  showTab('explanation');
542
  }
543