Spaces:
Sleeping
Sleeping
Update index.html
Browse files- 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');
|
| 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 |
-
|
| 377 |
-
|
| 378 |
-
const
|
| 379 |
-
const
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 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 |
-
|
| 394 |
-
body: JSON.stringify({ word: word, context: context })
|
| 395 |
});
|
| 396 |
|
| 397 |
if (!response.ok) {
|
| 398 |
-
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
const data = await response.json();
|
| 402 |
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
} catch (error) {
|
| 408 |
console.error('Error fetching context from AI:', error);
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
} finally {
|
| 412 |
-
|
| 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 |
|