Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Document Viewer with Flashcard Generation</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/epubjs/dist/epub.min.js"></script> | |
| <link rel="stylesheet" href="/static/css/styles.css"> | |
| <script src="/static/js/prompts.js"></script> | |
| <script src="/static/js/models.js"></script> | |
| </head> | |
| <body> | |
| <div id="top-bar"> | |
| <input type="file" id="file-input" accept=".pdf,.txt,.epub"> | |
| <span id="current-page">Page: 1</span> | |
| </div> | |
| <div id="left-panel"> | |
| <div id="pdf-viewer"></div> | |
| <div id="epub-viewer"></div> | |
| </div> | |
| <div id="right-panel"> | |
| <div id="top-controls"> | |
| <div id="settings-icon">βοΈ</div> | |
| <div id="dark-mode-toggle">π</div> | |
| <div id="page-navigation"> | |
| <button id="zoom-out-btn">-</button> | |
| <button id="zoom-in-btn">+</button> | |
| <input type="number" id="page-input" min="1" placeholder="Page #"> | |
| <button id="go-to-page-btn">Go</button> | |
| </div> | |
| </div> | |
| <div id="settings-panel" style="display: none;"> | |
| <input type="password" id="api-key-input" placeholder="Enter API Key"> | |
| <select id="model-select"></select> | |
| <textarea id="system-prompt" placeholder="Enter system prompt for flashcard generation"></textarea> | |
| <textarea id="explain-prompt" placeholder="Enter system prompt for explanation" style="display: none;"></textarea> | |
| <textarea id="language-prompt" placeholder="Enter system prompt for language mode"></textarea> | |
| <div id="language-buttons" style="display: none; margin-top: 10px;"> | |
| <button class="mode-btn" data-language="English">English</button> | |
| <button class="mode-btn" data-language="French">French</button> | |
| </div> | |
| </div> | |
| <div id="mode-toggle"> | |
| <button class="mode-btn selected" data-mode="flashcard">Flashcard</button> | |
| <button class="mode-btn" data-mode="explain">Explain</button> | |
| <button class="mode-btn" data-mode="language">Language</button> | |
| </div> | |
| <button id="submit-btn" style="display: block;">Generate</button> | |
| <div id="flashcards"></div> | |
| <div class="dropdown" id="collection-dropdown"> | |
| <button class="dropbtn" id="collection-dropbtn">Collection Options</button> | |
| <div class="dropdown-content" id="collection-dropdown-content"> | |
| <a href="#" id="add-to-collection-option">Add to Collection (0)</a> | |
| <a href="#" id="clear-collection-option">Clear Collection</a> | |
| <a href="#" id="export-csv-option" style="display: none;">Export Flashcards to CSV</a> | |
| <a href="#" id="export-json-option" style="display: none;">Export Flashcards to JSON</a> | |
| </div> | |
| </div> | |
| <div id="recent-files"> | |
| <h3>Recent Files</h3> | |
| <ul id="file-list"></ul> | |
| </div> | |
| <div id="highlight-instruction" style="font-size: 0.7em; color: #666; position: absolute; bottom: 5px; right: 5px;">Use Alt+Select to highlight text</div> | |
| </div> | |
| <!-- Explanation Modal --> | |
| <div id="explanationModal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close">×</span> | |
| <div id="explanationModalContent"></div> | |
| </div> | |
| </div> | |
| <script> | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js'; | |
| const fileInput = document.getElementById('file-input'); | |
| const pdfViewer = document.getElementById('pdf-viewer'); | |
| const modeToggle = document.getElementById('mode-toggle'); | |
| const systemPrompt = document.getElementById('system-prompt'); | |
| const submitBtn = document.getElementById('submit-btn'); | |
| const flashcardsContainer = document.getElementById('flashcards'); | |
| const apiKeyInput = document.getElementById('api-key-input'); | |
| const modelSelect = document.getElementById('model-select'); | |
| const recentPdfList = document.getElementById('recent-pdf-list'); | |
| let pdfDoc = null; | |
| let pageNum = 1; | |
| let pageRendering = false; | |
| let pageNumPending = null; | |
| let scale = 3; | |
| const minScale = 0.5; | |
| const maxScale = 5; | |
| let mode = 'flashcard'; | |
| let apiKey = ''; | |
| let currentFileName = ''; | |
| let currentPage = 1; | |
| let selectedModel = 'gemini/gemini-exp-1206'; | |
| let lastProcessedQuery = ''; | |
| let lastRequestTime = 0; | |
| const cooldownTime = 1000; // 1 second cooldown | |
| function renderPage(num) { | |
| pageRendering = true; | |
| pdfDoc.getPage(num).then(function (page) { | |
| const viewport = page.getViewport({ scale: scale }); | |
| const pixelRatio = window.devicePixelRatio || 1; | |
| const adjustedViewport = page.getViewport({ scale: scale * pixelRatio }); | |
| const pageDiv = document.createElement('div'); | |
| pageDiv.className = 'page'; | |
| pageDiv.dataset.pageNumber = num; | |
| pageDiv.style.width = `${viewport.width}px`; | |
| pageDiv.style.height = `${viewport.height}px`; | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.height = adjustedViewport.height; | |
| canvas.width = adjustedViewport.width; | |
| canvas.style.width = `${viewport.width}px`; | |
| canvas.style.height = `${viewport.height}px`; | |
| const renderContext = { | |
| canvasContext: ctx, | |
| viewport: adjustedViewport, | |
| enableWebGL: true, | |
| renderInteractiveForms: true, | |
| }; | |
| const renderTask = page.render(renderContext); | |
| renderTask.promise.then(function () { | |
| pageRendering = false; | |
| if (pageNumPending !== null) { | |
| renderPage(pageNumPending); | |
| pageNumPending = null; | |
| } | |
| }); | |
| pageDiv.appendChild(canvas); | |
| // Text layer | |
| const textLayerDiv = document.createElement('div'); | |
| textLayerDiv.className = 'text-layer'; | |
| textLayerDiv.style.width = `${viewport.width}px`; | |
| textLayerDiv.style.height = `${viewport.height}px`; | |
| pageDiv.appendChild(textLayerDiv); | |
| page.getTextContent().then(function (textContent) { | |
| pdfjsLib.renderTextLayer({ | |
| textContent: textContent, | |
| container: textLayerDiv, | |
| viewport: viewport, | |
| textDivs: [] | |
| }); | |
| }); | |
| pdfViewer.appendChild(pageDiv); | |
| // Attach language mode listener to the new page | |
| attachLanguageModeListener(pageDiv); | |
| // Render highlights for this page | |
| renderHighlights(); | |
| // Check if we need to load more pages | |
| if (num < pdfDoc.numPages && pdfViewer.scrollHeight <= window.innerHeight * 2) { | |
| renderPage(num + 1); | |
| } | |
| }); | |
| } | |
| function loadFile(file) { | |
| if (file.name.endsWith('.pdf')) { | |
| loadPDF(file); | |
| } else if (file.name.endsWith('.txt')) { | |
| loadTXT(file); | |
| } | |
| } | |
| function loadPDF(file) { | |
| const fileReader = new FileReader(); | |
| fileReader.onload = function () { | |
| const typedarray = new Uint8Array(this.result); | |
| pdfjsLib.getDocument(typedarray).promise.then(function (pdf) { | |
| pdfDoc = pdf; | |
| pdfViewer.innerHTML = ''; | |
| currentFileName = file.name; | |
| const lastPage = localStorage.getItem(`lastPage_${currentFileName}`); | |
| pageNum = lastPage ? Math.max(parseInt(lastPage) - 2, 1) : 1; | |
| loadScaleForCurrentFile(); | |
| renderPage(pageNum); | |
| updateCurrentPage(pageNum); | |
| hideHeaderPanel(); | |
| loadHighlights(); | |
| }); | |
| }; | |
| fileReader.readAsArrayBuffer(file); | |
| } | |
| function loadTXT(file) { | |
| const fileReader = new FileReader(); | |
| fileReader.onload = function () { | |
| const content = this.result; | |
| pdfViewer.innerHTML = ''; | |
| currentFileName = file.name; | |
| const textContainer = document.createElement('div'); | |
| textContainer.className = 'text-content'; | |
| textContainer.textContent = content; | |
| pdfViewer.appendChild(textContainer); | |
| hideHeaderPanel(); | |
| // Add event listeners for language mode | |
| attachLanguageModeListener(textContainer); | |
| }; | |
| fileReader.readAsText(file); | |
| } | |
| function hideHeaderPanel() { | |
| document.getElementById('top-bar').style.display = 'none'; | |
| } | |
| function goToPage(num) { | |
| if (num >= 1 && num <= pdfDoc.numPages) { | |
| pageNum = num; | |
| pdfViewer.innerHTML = ''; | |
| renderPage(pageNum); | |
| updateCurrentPage(pageNum); | |
| localStorage.setItem(`lastPage_${currentFileName}`, pageNum); | |
| } else { | |
| alert('Invalid page number'); | |
| } | |
| } | |
| function updateCurrentPage(num) { | |
| if (num !== currentPage) { | |
| currentPage = num; | |
| document.getElementById('current-page').textContent = `Page: ${num}`; | |
| document.getElementById('page-input').value = num; | |
| localStorage.setItem(`lastPage_${currentFileName}`, num); | |
| } | |
| } | |
| // Infinite scrolling with page tracking | |
| document.getElementById('left-panel').addEventListener('scroll', function () { | |
| if (this.scrollTop + this.clientHeight >= this.scrollHeight - 500) { | |
| if (pageNum < pdfDoc.numPages) { | |
| pageNum++; | |
| renderPage(pageNum); | |
| } | |
| } | |
| // Update current page based on scroll position | |
| const pages = document.querySelectorAll('.page'); | |
| for (let i = 0; i < pages.length; i++) { | |
| const page = pages[i]; | |
| const rect = page.getBoundingClientRect(); | |
| if (rect.top >= 0 && rect.bottom <= window.innerHeight) { | |
| const newPageNum = parseInt(page.dataset.pageNumber); | |
| updateCurrentPage(newPageNum); | |
| break; | |
| } | |
| } | |
| }); | |
| function handleLanguageMode(event, targetLanguage) { | |
| if (mode !== 'language') return; | |
| event.preventDefault(); | |
| const selection = window.getSelection(); | |
| if (selection.rangeCount > 0) { | |
| const range = selection.getRangeAt(0); | |
| const selectedText = selection.toString().trim(); | |
| if (selectedText) { | |
| const phrase = getPhrase(range); | |
| const currentTime = Date.now(); | |
| if (phrase !== lastProcessedQuery && currentTime - lastRequestTime >= cooldownTime) { | |
| lastProcessedQuery = phrase; | |
| lastRequestTime = currentTime; | |
| speakWord(selectedText); | |
| generateLanguageFlashcard(selectedText, phrase, targetLanguage); | |
| } | |
| } | |
| } | |
| } | |
| let voices = []; | |
| function populateVoiceList() { | |
| voices = speechSynthesis.getVoices(); | |
| } | |
| populateVoiceList(); | |
| if (speechSynthesis.onvoiceschanged !== undefined) { | |
| speechSynthesis.onvoiceschanged = populateVoiceList; | |
| } | |
| function speakWord(word) { | |
| console.log('Attempting to speak word:', word); | |
| const utterance = new SpeechSynthesisUtterance(word); | |
| utterance.rate = 0.8; // Slightly slower rate for clarity | |
| let englishVoice; | |
| if (voices.length > 1) { | |
| englishVoice = voices[2]; | |
| console.log('Using second voice in the list:', englishVoice.name); | |
| } else { | |
| englishVoice = voices.find(voice => voice.name === "Microsoft Zira Desktop - English (United States)") || | |
| voices.find(voice => /en/i.test(voice.lang)); | |
| if (englishVoice) { | |
| console.log('Using voice:', englishVoice.name); | |
| } else { | |
| console.log('No suitable English voice found. Using default voice.'); | |
| } | |
| } | |
| if (englishVoice) { | |
| utterance.voice = englishVoice; | |
| } | |
| try { | |
| speechSynthesis.speak(utterance); | |
| } catch (error) { | |
| console.error('Error initiating speech:', error); | |
| } | |
| } | |
| function getPhrase(range) { | |
| const sentenceStart = /[.!?]\s+[A-Z]|^[A-Z]/; | |
| const sentenceEnd = /[.!?](?=\s|$)/; | |
| let startNode = range.startContainer; | |
| let endNode = range.endContainer; | |
| let startOffset = range.startOffset; | |
| let endOffset = range.endOffset; | |
| // Expand to sentence boundaries | |
| while (startNode && startNode.textContent && !sentenceStart.test(startNode.textContent.slice(0, startOffset))) { | |
| if (startNode.previousSibling) { | |
| startNode = startNode.previousSibling; | |
| startOffset = startNode.textContent ? startNode.textContent.length : 0; | |
| } else if (startNode.parentNode && startNode.parentNode.previousSibling) { | |
| startNode = startNode.parentNode.previousSibling.lastChild; | |
| startOffset = startNode && startNode.textContent ? startNode.textContent.length : 0; | |
| } else { | |
| break; | |
| } | |
| } | |
| while (endNode && endNode.textContent && !sentenceEnd.test(endNode.textContent.slice(endOffset))) { | |
| if (endNode.nextSibling) { | |
| endNode = endNode.nextSibling; | |
| endOffset = 0; | |
| } else if (endNode.parentNode && endNode.parentNode.nextSibling) { | |
| endNode = endNode.parentNode.nextSibling.firstChild; | |
| endOffset = 0; | |
| } else { | |
| break; | |
| } | |
| } | |
| // Check if we have valid start and end nodes | |
| if (startNode && startNode.nodeType === Node.TEXT_NODE && | |
| endNode && endNode.nodeType === Node.TEXT_NODE && | |
| startNode.textContent && endNode.textContent) { | |
| const phraseRange = document.createRange(); | |
| phraseRange.setStart(startNode, startOffset); | |
| phraseRange.setEnd(endNode, endOffset); | |
| return phraseRange.toString().trim(); | |
| } else { | |
| // If we don't have valid nodes, return the original selection | |
| return range.toString().trim(); | |
| } | |
| } | |
| function getFullSentence(text, word) { | |
| const sentenceRegex = /[^.!?]+[.!?]+\s*/g; | |
| const sentences = text.match(sentenceRegex) || [text]; | |
| const matchingSentences = sentences.filter(sentence => | |
| new RegExp(`\\b${word}\\b`, 'i').test(sentence) | |
| ); | |
| if (matchingSentences.length === 0) { | |
| const wordIndex = text.indexOf(word); | |
| if (wordIndex !== -1) { | |
| const start = Math.max(0, wordIndex - 30); | |
| const end = Math.min(text.length, wordIndex + word.length + 30); | |
| return text.slice(start, end); | |
| } | |
| return text; | |
| } else if (matchingSentences.length === 1) { | |
| // If only one matching sentence, return it | |
| return matchingSentences[0].trim(); | |
| } else { | |
| // If multiple matching sentences, return them joined | |
| return matchingSentences.join(' ').trim(); | |
| } | |
| } | |
| async function callLLMAPI(prompt) { | |
| const response = await fetch('/generate_flashcard', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-API-Key': apiKey | |
| }, | |
| body: JSON.stringify({ | |
| prompt: prompt, | |
| model: selectedModel, | |
| mode: mode | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| return await response.json(); | |
| } | |
| async function generateLanguageFlashcard(word, phrase, targetLanguage) { | |
| const prompt = document.getElementById('language-prompt').value | |
| .replace('{word}', word) | |
| .replace('{phrase}', phrase) | |
| .replace('{targetLanguage}', targetLanguage); | |
| try { | |
| const response = await callLLMAPI(prompt); | |
| if (response.flashcard) { | |
| const flashcard = response.flashcard; | |
| const formattedFlashcard = { | |
| question: flashcard.question, | |
| answer: flashcard.answer, | |
| word: flashcard.word, | |
| translation: flashcard.translation | |
| }; | |
| console.log(formattedFlashcard); | |
| displayLanguageFlashcard(formattedFlashcard); | |
| } else { | |
| throw new Error('Invalid response from API'); | |
| } | |
| } catch (error) { | |
| console.error('Error calling LLM API:', error); | |
| alert('Failed to generate language flashcard. Please check your API key and try again.'); | |
| } | |
| } | |
| async function generateContent() { | |
| const selection = window.getSelection(); | |
| if (selection.rangeCount > 0 && selection.toString().trim() !== '') { | |
| const selectedText = selection.toString(); | |
| let prompt; | |
| if (mode === 'flashcard') { | |
| prompt = `${systemPrompt.value}\n\n${selectedText}`; | |
| } else if (mode === 'explain') { | |
| const explainPromptValue = document.getElementById('explain-prompt').value; | |
| prompt = `${explainPromptValue}\n\n${selectedText}`; | |
| } else { | |
| return; | |
| } | |
| // Disable the button and show notification | |
| submitBtn.disabled = true; | |
| submitBtn.style.backgroundColor = '#808080'; | |
| const notification = document.createElement('div'); | |
| notification.textContent = 'Generating...'; | |
| notification.style.position = 'fixed'; | |
| notification.style.top = '20px'; | |
| notification.style.right = '20px'; | |
| notification.style.padding = '10px'; | |
| notification.style.backgroundColor = 'rgba(0, 128, 0, 0.7)'; | |
| notification.style.color = 'white'; | |
| notification.style.borderRadius = '5px'; | |
| notification.style.zIndex = '1000'; | |
| document.body.appendChild(notification); | |
| try { | |
| const response = await callLLMAPI(prompt); | |
| if (mode === 'flashcard' && response.flashcards) { | |
| displayFlashcards(response.flashcards, true); | |
| } else if (mode === 'explain' && response.explanation) { | |
| displayExplanation(response.explanation); | |
| } else { | |
| throw new Error('Invalid response from API'); | |
| } | |
| } catch (error) { | |
| console.error('Error calling LLM API:', error); | |
| alert(`Failed to generate ${mode === 'flashcard' ? 'flashcards' : 'an explanation'}. Please check your API key and try again.`); | |
| } finally { | |
| setTimeout(() => { | |
| document.body.removeChild(notification); | |
| submitBtn.disabled = false; | |
| submitBtn.style.backgroundColor = ''; | |
| }, 3000); | |
| } | |
| } else { | |
| alert(`Please select some text to generate ${mode === 'flashcard' ? 'flashcards' : 'an explanation'}.`); | |
| } | |
| } | |
| function displayExplanation(explanation) { | |
| // Display in right panel | |
| const explanationElement = document.createElement('div'); | |
| explanationElement.className = 'explanation'; | |
| explanationElement.innerHTML = ` | |
| <h3>Explanation</h3> | |
| <div class="explanation-content">${explanation}</div> | |
| <button class="remove-btn">Remove</button> | |
| `; | |
| explanationElement.querySelector('.remove-btn').addEventListener('click', function () { | |
| explanationElement.remove(); | |
| }); | |
| flashcardsContainer.appendChild(explanationElement); | |
| // Display in modal | |
| const modal = document.getElementById('explanationModal'); | |
| const modalContent = document.getElementById('explanationModalContent'); | |
| const closeBtn = document.getElementsByClassName('close')[0]; | |
| // Convert markdown to HTML | |
| const converter = new showdown.Converter(); | |
| const htmlContent = converter.makeHtml(explanation); | |
| modalContent.innerHTML = htmlContent; | |
| modal.style.display = 'block'; | |
| closeBtn.onclick = function () { | |
| modal.style.display = 'none'; | |
| } | |
| window.onclick = function (event) { | |
| if (event.target == modal) { | |
| modal.style.display = 'none'; | |
| } | |
| } | |
| } | |
| function displayFlashcards(flashcards, append = false) { | |
| if (!append) { | |
| flashcardsContainer.innerHTML = ''; // Clear existing flashcards only if not appending | |
| } | |
| flashcards.forEach(flashcard => { | |
| const flashcardElement = document.createElement('div'); | |
| flashcardElement.className = 'flashcard'; | |
| flashcardElement.innerHTML = ` | |
| <strong>Q: ${flashcard.question}</strong><br> | |
| A: ${flashcard.answer} | |
| <button class="remove-btn">Remove</button> | |
| `; | |
| flashcardElement.querySelector('.remove-btn').addEventListener('click', function () { | |
| flashcardElement.remove(); | |
| updateExportButtonVisibility(); | |
| }); | |
| // Prepend new flashcard to the container to show it at the top | |
| flashcardsContainer.insertBefore(flashcardElement, flashcardsContainer.firstChild); | |
| }); | |
| updateExportButtonVisibility(); | |
| } | |
| function displayLanguageFlashcard(flashcard) { | |
| const flashcardElement = document.createElement('div'); | |
| flashcardElement.className = 'flashcard language-flashcard'; | |
| flashcardElement.dataset.question = flashcard.question; | |
| flashcardElement.dataset.word = flashcard.word; | |
| flashcardElement.dataset.translation = flashcard.translation; | |
| flashcardElement.dataset.answer = flashcard.answer; | |
| flashcardElement.innerHTML = ` | |
| <div style="font-size: 1.2em; margin-bottom: 10px;"><b>${flashcard.word}</b>: ${flashcard.translation}</div> | |
| <div>- ${flashcard.answer}</div> | |
| <button class="remove-btn">Remove</button> | |
| `; | |
| flashcardElement.querySelector('.remove-btn').addEventListener('click', function () { | |
| flashcardElement.remove(); | |
| updateExportButtonVisibility(); | |
| }); | |
| // Prepend new language flashcard at the top of the container | |
| flashcardsContainer.insertBefore(flashcardElement, flashcardsContainer.firstChild); | |
| updateExportButtonVisibility(); | |
| } | |
| let flashcardCollectionCount = 0; | |
| let languageCollectionCount = 0; | |
| let collectedFlashcards = []; | |
| let collectedLanguageFlashcards = []; | |
| function addToCollection() { | |
| const newFlashcards = Array.from(document.querySelectorAll('.flashcard:not(.in-collection)')).map(flashcard => { | |
| if (flashcard.classList.contains('language-flashcard')) { | |
| const word = flashcard.dataset.word; | |
| const translation = flashcard.dataset.translation; | |
| const answer = flashcard.dataset.answer; | |
| const question = flashcard.dataset.question; | |
| return { | |
| word: word, | |
| phrase: question, | |
| translationAnswer: `${translation.trim()}\n${answer.trim()}` | |
| }; | |
| } else { | |
| const question = flashcard.querySelector('strong').textContent.slice(3); | |
| const answer = flashcard.innerHTML.split('<br>')[1].split('<button')[0].trim().slice(3); | |
| return { | |
| phrase: question, | |
| translationAnswer: answer | |
| }; | |
| } | |
| }); | |
| if (mode === 'language') { | |
| collectedLanguageFlashcards = collectedLanguageFlashcards.concat(newFlashcards); | |
| updateCollectionCount(newFlashcards.length, 'language'); | |
| } else { | |
| collectedFlashcards = collectedFlashcards.concat(newFlashcards); | |
| updateCollectionCount(newFlashcards.length, 'flashcard'); | |
| } | |
| clearDisplayedFlashcards(); | |
| updateExportButtonVisibility(); | |
| } | |
| function clearDisplayedFlashcards() { | |
| flashcardsContainer.innerHTML = ''; | |
| } | |
| function updateCollectionCount(change, collectionType) { | |
| if (collectionType === 'language') { | |
| languageCollectionCount += change; | |
| localStorage.setItem('languageCollectionCount', languageCollectionCount); | |
| localStorage.setItem('collectedLanguageFlashcards', JSON.stringify(collectedLanguageFlashcards)); | |
| } else { | |
| flashcardCollectionCount += change; | |
| localStorage.setItem('flashcardCollectionCount', flashcardCollectionCount); | |
| localStorage.setItem('collectedFlashcards', JSON.stringify(collectedFlashcards)); | |
| } | |
| updateAddToCollectionButtonText(); | |
| } | |
| function updateAddToCollectionButtonText() { | |
| var addToCollectionOption = document.getElementById('add-to-collection-option'); | |
| var count = mode === 'language' ? languageCollectionCount : flashcardCollectionCount; | |
| addToCollectionOption.textContent = `Add to Collection (${count})`; | |
| } | |
| // Initialize collection counts and flashcards from localStorage | |
| flashcardCollectionCount = parseInt(localStorage.getItem('flashcardCollectionCount')) || 0; | |
| languageCollectionCount = parseInt(localStorage.getItem('languageCollectionCount')) || 0; | |
| collectedFlashcards = JSON.parse(localStorage.getItem('collectedFlashcards')) || []; | |
| collectedLanguageFlashcards = JSON.parse(localStorage.getItem('collectedLanguageFlashcards')) || []; | |
| updateAddToCollectionButtonText(); | |
| document.getElementById('add-to-collection-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| addToCollection(); | |
| }); | |
| function updateExportButtonVisibility() { | |
| var csvExportOption = document.getElementById('export-csv-option'); | |
| var jsonExportOption = document.getElementById('export-json-option'); | |
| var currentCollection = mode === 'language' ? collectedLanguageFlashcards : collectedFlashcards; | |
| var count = currentCollection.length; | |
| csvExportOption.style.display = count > 0 ? 'block' : 'none'; | |
| csvExportOption.textContent = `Export Flashcards to CSV (${count})`; | |
| jsonExportOption.style.display = count > 0 ? 'block' : 'none'; | |
| jsonExportOption.textContent = `Export Flashcards to JSON (${count})`; | |
| } | |
| function exportToCSV() { | |
| let csvContent = "data:text/csv;charset=utf-8,"; | |
| const currentCollection = mode === 'language' ? collectedLanguageFlashcards : collectedFlashcards; | |
| const removeQuotes = str => str.replace(/"/g, ''); | |
| if (mode === 'language') { | |
| currentCollection.forEach(({ phrase, translationAnswer }) => { | |
| const [translation, answer] = translationAnswer.split('\n'); | |
| csvContent += `${removeQuotes(phrase)};- ${removeQuotes(translation)}<br>- ${removeQuotes(answer)}\n`; | |
| }); | |
| } else { | |
| currentCollection.forEach(({ phrase, translationAnswer }) => { | |
| csvContent += `${removeQuotes(phrase)};${removeQuotes(translationAnswer)}\n`; | |
| }); | |
| } | |
| const encodedUri = encodeURI(csvContent); | |
| const link = document.createElement("a"); | |
| link.setAttribute("href", encodedUri); | |
| link.setAttribute("download", `${mode}_flashcards.csv`); | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } | |
| // New function: Export flashcards as JSON | |
| function exportToJSON() { | |
| const currentCollection = mode === 'language' ? collectedLanguageFlashcards : collectedFlashcards; | |
| const dataStr = JSON.stringify(currentCollection, null, 2); | |
| const jsonContent = "data:text/json;charset=utf-8," + encodeURIComponent(dataStr); | |
| const link = document.createElement("a"); | |
| link.setAttribute("href", jsonContent); | |
| link.setAttribute("download", `${mode}_flashcards.json`); | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } | |
| document.getElementById('export-csv-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| exportToCSV(); | |
| }); | |
| document.getElementById('export-json-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| exportToJSON(); | |
| }); | |
| function clearCollection() { | |
| if (confirm('Are you sure you want to clear the entire collection? This action cannot be undone.')) { | |
| if (mode === 'language') { | |
| collectedLanguageFlashcards = []; | |
| languageCollectionCount = 0; | |
| localStorage.removeItem('collectedLanguageFlashcards'); | |
| localStorage.removeItem('languageCollectionCount'); | |
| } else { | |
| collectedFlashcards = []; | |
| flashcardCollectionCount = 0; | |
| localStorage.removeItem('collectedFlashcards'); | |
| localStorage.removeItem('flashcardCollectionCount'); | |
| } | |
| updateCollectionCount(0, mode); | |
| updateExportButtonVisibility(); | |
| } | |
| } | |
| document.getElementById('clear-collection-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| clearCollection(); | |
| }); | |
| // Initialize export button visibility | |
| updateExportButtonVisibility(); | |
| function addRecentFile(filename) { | |
| let recentFiles = JSON.parse(localStorage.getItem('recentFiles')) || []; | |
| recentFiles = recentFiles.filter(file => file.filename !== filename); | |
| recentFiles.unshift({ filename: filename, date: new Date().toISOString() }); | |
| recentFiles = recentFiles.slice(0, 5); // Keep only the 5 most recent | |
| localStorage.setItem('recentFiles', JSON.stringify(recentFiles)); | |
| loadRecentFiles(); | |
| } | |
| function updateRecentPDFsList() { | |
| const recentPDFs = JSON.parse(localStorage.getItem('recentPDFs')) || []; | |
| recentPdfList.innerHTML = ''; | |
| recentPDFs.forEach(pdf => { | |
| const li = document.createElement('li'); | |
| li.textContent = `${pdf.filename} (${new Date(pdf.date).toLocaleDateString()})`; | |
| recentPdfList.appendChild(li); | |
| }); | |
| } | |
| fileInput.addEventListener('change', function (e) { | |
| const file = e.target.files[0]; | |
| if (file.type !== 'application/pdf' && file.type !== 'text/plain' && file.type !== 'application/epub+zip') { | |
| console.error('Error: Not a PDF, TXT, or EPUB file'); | |
| return; | |
| } | |
| loadFile(file); | |
| addRecentFile(file.name); | |
| this.nextElementSibling.textContent = file.name; | |
| }); | |
| // Add a span next to the file input to display the selected file name | |
| const fileNameDisplay = document.createElement('span'); | |
| fileNameDisplay.style.marginLeft = '10px'; | |
| fileInput.parentNode.insertBefore(fileNameDisplay, fileInput.nextSibling); | |
| function handleGoToPage() { | |
| const pageInput = document.getElementById('page-input'); | |
| const pageNumber = parseInt(pageInput.value); | |
| goToPage(pageNumber); | |
| } | |
| document.getElementById('go-to-page-btn').addEventListener('click', handleGoToPage); | |
| document.getElementById('page-input').addEventListener('keyup', function (event) { | |
| if (event.key === 'Enter') { | |
| handleGoToPage(); | |
| } | |
| }); | |
| function calculateZoomStep(currentScale) { | |
| return Math.max(0.1, Math.min(0.25, currentScale * 0.1)); | |
| } | |
| document.getElementById('zoom-in-btn').addEventListener('click', function() { | |
| if (scale < maxScale) { | |
| const step = calculateZoomStep(scale); | |
| scale = Math.min(maxScale, scale + step); | |
| reRenderPDF(); | |
| saveScaleForCurrentFile(); | |
| } | |
| }); | |
| document.getElementById('zoom-out-btn').addEventListener('click', function() { | |
| if (scale > minScale) { | |
| const step = calculateZoomStep(scale); | |
| scale = Math.max(minScale, scale - step); | |
| reRenderPDF(); | |
| saveScaleForCurrentFile(); | |
| } | |
| }); | |
| function reRenderPDF() { | |
| pdfViewer.innerHTML = ''; | |
| renderPage(pageNum); | |
| } | |
| function saveScaleForCurrentFile() { | |
| if (currentFileName) { | |
| localStorage.setItem(`scale_${currentFileName}`, scale); | |
| } | |
| } | |
| function loadScaleForCurrentFile() { | |
| if (currentFileName) { | |
| const savedScale = localStorage.getItem(`scale_${currentFileName}`); | |
| if (savedScale) { | |
| scale = parseFloat(savedScale); | |
| } | |
| } | |
| } | |
| const modeButtons = document.querySelectorAll('.mode-btn'); | |
| modeButtons.forEach(button => { | |
| button.addEventListener('click', function () { | |
| modeButtons.forEach(btn => btn.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| mode = this.dataset.mode; | |
| pdfViewer.style.cursor = mode === 'language' ? 'text' : 'default'; | |
| document.getElementById('language-buttons').style.display = mode === 'language' ? 'flex' : 'none'; | |
| systemPrompt.style.display = mode === 'flashcard' ? 'block' : 'none'; | |
| document.getElementById('explain-prompt').style.display = mode === 'explain' ? 'block' : 'none'; | |
| document.getElementById('language-prompt').style.display = mode === 'language' ? 'block' : 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Update the collection button text and export button visibility | |
| updateAddToCollectionButtonText(); | |
| updateExportButtonVisibility(); | |
| }); | |
| }); | |
| const languageButtons = document.querySelectorAll('#language-buttons .mode-btn'); | |
| languageButtons.forEach(button => { | |
| button.addEventListener('click', function (event) { | |
| event.preventDefault(); | |
| languageButtons.forEach(btn => btn.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| const targetLanguage = this.dataset.language; | |
| saveLanguageChoice(targetLanguage); | |
| // Ensure the Language mode button remains selected | |
| document.querySelector('.mode-btn[data-mode="language"]').classList.add('selected'); | |
| // Keep language buttons visible and Generate button visible | |
| submitBtn.style.display = 'block'; | |
| // Set the mode to 'language' | |
| mode = 'language'; | |
| }); | |
| }); | |
| let highlights = []; | |
| function attachLanguageModeListener(container) { | |
| container.addEventListener('mouseup', function (event) { | |
| if (event.altKey) { | |
| const selection = window.getSelection(); | |
| if (selection.rangeCount > 0) { | |
| const range = selection.getRangeAt(0); | |
| const selectedText = selection.toString().trim(); | |
| console.log(selectedText); | |
| if (selectedText !== '') { | |
| console.log(range, container); | |
| const highlight = createHighlight(range, container); | |
| highlights.push(highlight); | |
| saveHighlights(); | |
| } | |
| } | |
| } | |
| }); | |
| container.addEventListener('dblclick', function (event) { | |
| if (mode === 'language') { | |
| const selection = window.getSelection(); | |
| const range = selection.getRangeAt(0); | |
| const word = selection.toString().trim(); | |
| if (word !== '' && word.length < 20) { | |
| // Highlight the selected word | |
| const span = document.createElement('span'); | |
| span.style.backgroundColor = 'rgba(255, 255, 0, 0.5)'; | |
| span.textContent = word; | |
| range.deleteContents(); | |
| range.insertNode(span); | |
| const selectedLanguageButton = document.querySelector('#language-buttons .mode-btn.selected'); | |
| if (selectedLanguageButton) { | |
| const targetLanguage = selectedLanguageButton.dataset.language; | |
| const phrase = getPhrase(range, word); | |
| generateLanguageFlashcard(word, phrase, targetLanguage); | |
| speakWord(word); | |
| } else { | |
| console.error('No language selected'); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function createHighlight(range, pageDiv) { | |
| const highlight = document.createElement('div'); | |
| highlight.className = 'highlight'; | |
| highlight.style.position = 'absolute'; | |
| highlight.style.backgroundColor = 'rgba(255, 255, 0, 0.3)'; | |
| highlight.style.pointerEvents = 'none'; | |
| const rect = range.getBoundingClientRect(); | |
| const pageBounds = pageDiv.getBoundingClientRect(); | |
| highlight.style.left = (rect.left - pageBounds.left) + 'px'; | |
| highlight.style.top = (rect.top - pageBounds.top) + 'px'; | |
| highlight.style.width = rect.width + 'px'; | |
| highlight.style.height = rect.height + 'px'; | |
| pageDiv.appendChild(highlight); | |
| return { | |
| element: highlight, | |
| pageNumber: parseInt(pageDiv.dataset.pageNumber), | |
| rects: [{ | |
| left: rect.left - pageBounds.left, | |
| top: rect.top - pageBounds.top, | |
| width: rect.width, | |
| height: rect.height | |
| }] | |
| }; | |
| } | |
| function saveHighlights() { | |
| localStorage.setItem('pdfHighlights', JSON.stringify(highlights)); | |
| } | |
| function loadHighlights() { | |
| const savedHighlights = JSON.parse(localStorage.getItem('pdfHighlights')) || []; | |
| highlights = savedHighlights; | |
| renderHighlights(); | |
| } | |
| function renderHighlights() { | |
| highlights.forEach(highlight => { | |
| const pageDiv = document.querySelector(`.page[data-page-number="${highlight.pageNumber}"]`); | |
| if (pageDiv) { | |
| const newHighlight = document.createElement('div'); | |
| newHighlight.className = 'highlight'; | |
| newHighlight.style.position = 'absolute'; | |
| newHighlight.style.backgroundColor = 'rgba(255, 255, 0, 0.3)'; | |
| newHighlight.style.pointerEvents = 'none'; | |
| const pageBounds = pageDiv.getBoundingClientRect(); | |
| const scale = parseFloat(pageDiv.style.width) / pageBounds.width; | |
| highlight.rects.forEach(rect => { | |
| const highlightRect = document.createElement('div'); | |
| highlightRect.style.position = 'absolute'; | |
| highlightRect.style.left = (rect.left * scale) + 'px'; | |
| highlightRect.style.top = (rect.top * scale) + 'px'; | |
| highlightRect.style.width = (rect.width * scale) + 'px'; | |
| highlightRect.style.height = (rect.height * scale) + 'px'; | |
| highlightRect.style.backgroundColor = 'inherit'; | |
| newHighlight.appendChild(highlightRect); | |
| }); | |
| pageDiv.appendChild(newHighlight); | |
| } | |
| }); | |
| } | |
| function getPhrase(range, word) { | |
| let startNode = range.startContainer; | |
| let endNode = range.endContainer; | |
| let startOffset = Math.max(0, range.startOffset - 50); | |
| let endOffset = Math.min(endNode.length, range.endOffset + 50); | |
| // Extract the phrase | |
| let phrase = ''; | |
| let currentNode = startNode; | |
| while (currentNode) { | |
| if (currentNode.nodeType === Node.TEXT_NODE) { | |
| const text = currentNode.textContent; | |
| const start = currentNode === startNode ? startOffset : 0; | |
| const end = currentNode === endNode ? endOffset : text.length; | |
| phrase += text.slice(start, end); | |
| } | |
| if (currentNode === endNode) break; | |
| currentNode = currentNode.nextSibling; | |
| } | |
| // Ensure the word is bolded in the phrase | |
| const wordRegex = new RegExp(`\\b${word}\\b`, 'gi'); | |
| phrase = phrase.replace(wordRegex, `<b>$&</b>`); | |
| return phrase.trim(); | |
| } | |
| function saveLanguageChoice(language) { | |
| localStorage.setItem('selectedLanguage', language); | |
| } | |
| function loadLanguageChoice() { | |
| return localStorage.getItem('selectedLanguage') || 'English'; | |
| } | |
| function setLanguageButton(language) { | |
| const languageButton = document.querySelector(`#language-buttons .mode-btn[data-language="${language}"]`); | |
| if (languageButton) { | |
| languageButtons.forEach(btn => btn.classList.remove('selected')); | |
| languageButton.classList.add('selected'); | |
| } | |
| } | |
| submitBtn.addEventListener('click', function() { | |
| if (mode === 'language') { | |
| const selection = window.getSelection(); | |
| if (selection.rangeCount > 0) { | |
| const range = selection.getRangeAt(0); | |
| const selectedText = selection.toString().trim(); | |
| if (selectedText !== '') { | |
| let selectedLanguageButton = document.querySelector('#language-buttons .mode-btn.selected'); | |
| if (!selectedLanguageButton) { | |
| // Fallback: use the default language from localStorage | |
| const defaultLanguage = loadLanguageChoice(); | |
| setLanguageButton(defaultLanguage); | |
| selectedLanguageButton = document.querySelector('#language-buttons .mode-btn.selected'); | |
| } | |
| if (selectedLanguageButton) { | |
| const targetLanguage = selectedLanguageButton.dataset.language; | |
| const phrase = getPhrase(range, selectedText); | |
| generateLanguageFlashcard(selectedText, phrase, targetLanguage); | |
| speakWord(selectedText); | |
| } | |
| } else { | |
| alert('Please select some text first'); | |
| } | |
| } else { | |
| alert('Please select some text first'); | |
| } | |
| } else { | |
| generateContent(); | |
| } | |
| }); | |
| apiKeyInput.addEventListener('change', function () { | |
| apiKey = this.value; | |
| localStorage.setItem('lastWorkingAPIKey', apiKey); | |
| }); | |
| // Load last working API key | |
| const lastWorkingAPIKey = localStorage.getItem('lastWorkingAPIKey'); | |
| if (lastWorkingAPIKey) { | |
| apiKeyInput.value = lastWorkingAPIKey; | |
| apiKey = lastWorkingAPIKey; | |
| } | |
| // Infinite scrolling | |
| document.getElementById('left-panel').addEventListener('scroll', function () { | |
| if (this.scrollTop + this.clientHeight >= this.scrollHeight - 500) { | |
| if (pageNum < pdfDoc.numPages) { | |
| pageNum++; | |
| renderPage(pageNum); | |
| } | |
| } | |
| }); | |
| function loadRecentFiles() { | |
| fetch('/get_recent_files') | |
| .then(response => response.json()) | |
| .then(recentFiles => { | |
| const fileList = document.getElementById('file-list'); | |
| fileList.innerHTML = ''; | |
| recentFiles.forEach(file => { | |
| const li = document.createElement('li'); | |
| const a = document.createElement('a'); | |
| a.href = '#'; | |
| a.textContent = `${file.filename} (${new Date(file.date).toLocaleDateString()})`; | |
| a.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| fetch(`/open_pdf/${file.filename}`) | |
| .then(response => response.blob()) | |
| .then(blob => { | |
| const fileType = file.filename.toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'text/plain'; | |
| const newFile = new File([blob], file.filename, { type: fileType }); | |
| loadFile(newFile); | |
| }) | |
| .catch(error => console.error('Error:', error)); | |
| }); | |
| li.appendChild(a); | |
| fileList.appendChild(li); | |
| }); | |
| }) | |
| .catch(error => console.error('Error loading recent files:', error)); | |
| } | |
| // Call loadRecentFiles when the page loads | |
| window.addEventListener('load', loadRecentFiles); | |
| // Update recent files list after uploading a new file | |
| function uploadFile(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| fetch('/upload_pdf', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.message) { | |
| console.log(data.message); | |
| loadFile(file); | |
| loadRecentFiles(); // Reload the recent files list | |
| } else { | |
| console.error(data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| }); | |
| } | |
| // Update loadFile function to reload recent files list | |
| let book; | |
| let rendition; | |
| let currentScale = 100; | |
| function loadFile(file) { | |
| const pdfViewer = document.getElementById('pdf-viewer'); | |
| const epubViewer = document.getElementById('epub-viewer'); | |
| // Hide both viewers initially | |
| pdfViewer.style.display = 'none'; | |
| epubViewer.style.display = 'none'; | |
| if (file.name.endsWith('.pdf')) { | |
| pdfViewer.style.display = 'block'; | |
| loadPDF(file); | |
| } else if (file.name.endsWith('.txt')) { | |
| pdfViewer.style.display = 'block'; // Assuming TXT files use the PDF viewer | |
| loadTXT(file); | |
| } else if (file.name.endsWith('.epub')) { | |
| epubViewer.style.display = 'block'; | |
| loadEPUB(file); | |
| } | |
| } | |
| function loadEPUB(file) { | |
| console.log('loadEPUB function called with file:', file.name); | |
| const epubContainer = document.getElementById('epub-viewer'); | |
| if (!epubContainer) { | |
| console.error('EPUB viewer container not found'); | |
| return; | |
| } | |
| epubContainer.innerHTML = ''; // Clear previous content | |
| epubContainer.style.display = 'block'; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| console.log('FileReader onload event fired'); | |
| const arrayBuffer = e.target.result; | |
| try { | |
| book = ePub(arrayBuffer); | |
| console.log('EPUB book object created:', book); | |
| book.ready.then(() => { | |
| console.log('EPUB book is ready'); | |
| rendition = book.renderTo('epub-viewer', { | |
| width: '100%', | |
| height: '100%', | |
| spread: 'always', | |
| sandbox: 'allow-scripts' | |
| }); | |
| console.log('Rendition object created:', rendition); | |
| rendition.display().then(() => { | |
| console.log('EPUB content displayed'); | |
| setupNavigation(); | |
| }).catch(error => { | |
| console.error('Error displaying EPUB content:', error); | |
| epubContainer.innerHTML = 'Error displaying EPUB content. Please check console for details.'; | |
| }); | |
| if (document.getElementById('pdf-viewer')) { | |
| document.getElementById('pdf-viewer').style.display = 'none'; | |
| } | |
| }).catch(error => { | |
| console.error('Error in book.ready:', error); | |
| epubContainer.innerHTML = 'Error preparing EPUB. Please check console for details.'; | |
| }); | |
| } catch (error) { | |
| console.error('Error creating EPUB book object:', error); | |
| epubContainer.innerHTML = 'Error loading EPUB. Please check console for details.'; | |
| } | |
| }; | |
| reader.onerror = function(e) { | |
| console.error('Error reading file:', e); | |
| epubContainer.innerHTML = 'Error reading file. Please try again.'; | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| function setupNavigation() { | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const zoomInBtn = document.getElementById('zoom-in-btn'); | |
| const zoomOutBtn = document.getElementById('zoom-out-btn'); | |
| if (prevBtn) prevBtn.onclick = prevPage; | |
| if (nextBtn) nextBtn.onclick = nextPage; | |
| if (zoomInBtn) zoomInBtn.onclick = zoomIn; | |
| if (zoomOutBtn) zoomOutBtn.onclick = zoomOut; | |
| // Enable keyboard navigation | |
| document.addEventListener('keydown', handleKeyPress); | |
| } | |
| function prevPage() { | |
| if (rendition) rendition.prev(); | |
| } | |
| function nextPage() { | |
| if (rendition) rendition.next(); | |
| } | |
| function zoomIn() { | |
| if (rendition) { | |
| currentScale += 10; | |
| setZoom(); | |
| } | |
| } | |
| function zoomOut() { | |
| if (rendition) { | |
| currentScale -= 10; | |
| if (currentScale < 50) currentScale = 50; // Prevent zooming out too much | |
| setZoom(); | |
| } | |
| } | |
| function setZoom() { | |
| if (rendition) { | |
| rendition.themes.fontSize(`${currentScale}%`); | |
| } | |
| } | |
| function handleKeyPress(e) { | |
| switch(e.key) { | |
| case "ArrowLeft": | |
| prevPage(); | |
| break; | |
| case "ArrowRight": | |
| nextPage(); | |
| break; | |
| } | |
| } | |
| // Save current page before unloading | |
| window.addEventListener('beforeunload', function () { | |
| if (currentFileName) { | |
| localStorage.setItem(`lastPage_${currentFileName}`, pageNum); | |
| } | |
| }); | |
| // Initialize recent PDFs list | |
| window.onload = function () { | |
| loadRecentFiles(); | |
| // Add event listener for settings icon | |
| document.getElementById('settings-icon').addEventListener('click', function () { | |
| const settingsPanel = document.getElementById('settings-panel'); | |
| settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none'; | |
| }); | |
| // Remove 'selected' class from main mode buttons only | |
| document.getElementById('mode-toggle').querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('selected')); | |
| // Set default mode to language | |
| mode = 'language'; | |
| document.querySelector('.mode-btn[data-mode="language"]').classList.add('selected'); | |
| document.getElementById('language-buttons').style.display = 'flex'; | |
| document.getElementById('submit-btn').style.display = 'block'; | |
| systemPrompt.style.display = 'none'; | |
| document.getElementById('explain-prompt').style.display = 'none'; | |
| document.getElementById('language-prompt').style.display = 'block'; | |
| // Set default language to English if not already set | |
| if (!localStorage.getItem('selectedLanguage')) { | |
| saveLanguageChoice('English'); | |
| } | |
| // Load and set the saved language choice | |
| const savedLanguage = loadLanguageChoice(); | |
| setLanguageButton(savedLanguage); | |
| }; | |
| fileInput.addEventListener('change', function (e) { | |
| const file = e.target.files[0]; | |
| if (file.type !== 'application/pdf' && file.type !== 'text/plain' && file.type !== 'application/epub+zip') { | |
| console.error('Error: Not a PDF, TXT, or EPUB file'); | |
| return; | |
| } | |
| uploadFile(file); | |
| }); | |
| function uploadFile(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| fetch('/upload_file', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.message) { | |
| console.log(data.message); | |
| loadFile(file); | |
| loadRecentFiles(); | |
| addRecentFile(file.name); | |
| } else { | |
| console.error(data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| }); | |
| } | |
| document.addEventListener("DOMContentLoaded", () => { | |
| // Ensure the variables are defined from /static/js/prompts.js | |
| if (typeof FLASHCARD_PROMPT !== 'undefined') { | |
| document.getElementById('system-prompt').value = FLASHCARD_PROMPT; | |
| } | |
| if (typeof EXPLAIN_PROMPT !== 'undefined') { | |
| document.getElementById('explain-prompt').value = EXPLAIN_PROMPT; | |
| } | |
| if (typeof LANGUAGE_PROMPT !== 'undefined') { | |
| document.getElementById('language-prompt').value = LANGUAGE_PROMPT; | |
| } | |
| // Populate the model select options | |
| const modelSelect = document.getElementById('model-select'); | |
| availableModels.forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model; | |
| option.textContent = model; | |
| modelSelect.appendChild(option); | |
| }); | |
| // Set default model to the first one in the list instead of hard-coding Gemini | |
| const firstModel = availableModels[0]; // New: use the first model from availableModels | |
| modelSelect.value = firstModel; // Update the select element | |
| selectedModel = firstModel; // Update the global selectedModel value | |
| // Update API key placeholder based on selected model on change | |
| modelSelect.addEventListener('change', function() { | |
| selectedModel = this.value; | |
| const requiredKey = MODEL_API_KEY_MAPPING[selectedModel]; | |
| apiKeyInput.placeholder = `Enter ${requiredKey}`; | |
| }); | |
| // Set initial API key placeholder based on the first model | |
| const initialKey = MODEL_API_KEY_MAPPING[firstModel]; // Updated to use firstModel | |
| apiKeyInput.placeholder = `Enter ${initialKey}`; | |
| }); | |
| // Ensure these run after the DOM is loaded | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Dropdown toggle logic for the collection dropdown | |
| var collectionDropbtn = document.getElementById('collection-dropbtn'); | |
| var dropdownContent = document.getElementById('collection-dropdown-content'); | |
| collectionDropbtn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| dropdownContent.classList.toggle('show'); | |
| this.classList.toggle('active'); | |
| }); | |
| document.addEventListener('click', function(e) { | |
| if (!dropdownContent.contains(e.target)) { | |
| dropdownContent.classList.remove('show'); | |
| collectionDropbtn.classList.remove('active'); | |
| } | |
| }); | |
| // Update event listeners for dropdown options instead of separate buttons | |
| document.getElementById('add-to-collection-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| addToCollection(); | |
| }); | |
| document.getElementById('clear-collection-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| clearCollection(); | |
| }); | |
| document.getElementById('export-csv-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| exportToCSV(); | |
| }); | |
| document.getElementById('export-json-option').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| exportToJSON(); | |
| }); | |
| }); | |
| // Dark mode toggle functionality | |
| const darkModeToggle = document.getElementById('dark-mode-toggle'); | |
| let isDarkMode = localStorage.getItem('darkMode') === 'true'; | |
| // Initialize dark mode state | |
| if (isDarkMode) { | |
| document.body.classList.add('dark-mode'); | |
| darkModeToggle.textContent = 'βοΈ'; | |
| } | |
| darkModeToggle.addEventListener('click', () => { | |
| isDarkMode = !isDarkMode; | |
| document.body.classList.toggle('dark-mode'); | |
| darkModeToggle.textContent = isDarkMode ? 'βοΈ' : 'π'; | |
| localStorage.setItem('darkMode', isDarkMode); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |