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"> | |
</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="page-navigation"> | |
<button id="zoom-out-btn">-</button> | |
<button id="zoom-in-btn">+</button> | |
<input type="number" id="page-input" min="1" placeholder="Go to 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 Claude API Key"> | |
<select id="model-select"> | |
<option value="claude-3-5-sonnet-20240620">Claude 3.5 Sonnet</option> | |
<option value="claude-3-haiku-20240307">Claude 3 Haiku</option> | |
</select> | |
<textarea id="system-prompt" placeholder="Enter system prompt for flashcard generation">Generate concise flashcards based on the following text. The number of flashcards should be proportional to the text's length and complexity, with a minimum of 1 and a maximum of 10. Each flashcard should have a question (Q:) that tests a key concept and an answer (A:) that is brief but complete. Ensure that the flashcards cover different aspects of the text when possible. Use <b> tags to emphasize important words or phrases in both questions and answers. Cite the short code or example to the question if needed. | |
Example: | |
Text: "In parallel computing, load balancing refers to the practice of distributing computational work evenly across multiple processing units. This is crucial for maximizing efficiency and minimizing idle time. Dynamic load balancing adjusts the distribution of work during runtime, while static load balancing determines the distribution before execution begins." | |
Q: What is the primary goal of <b>load balancing</b> in parallel computing? | |
A: To <b>distribute work evenly</b> across processing units, maximizing efficiency and minimizing idle time. | |
Q: How does <b>dynamic load balancing</b> differ from <b>static load balancing</b>? | |
A: Dynamic balancing <b>adjusts work distribution during runtime</b>, while static balancing <b>determines distribution before execution</b>. | |
That was example, now generate flashcards for this text: | |
</textarea> | |
<textarea id="explain-prompt" placeholder="Enter system prompt for explanation" style="display: none;">Explain the following text in simple terms, focusing on the main concepts and their relationships. Use clear and concise language, and break down complex ideas into easily understandable parts. If there are any technical terms, provide brief explanations for them. Return your explanation in markdown format. | |
Now explain this text:</textarea> | |
<textarea id="language-prompt" placeholder="Enter system prompt for language mode">Explain the word in the phrase in {targetLanguage} using this format: | |
T: [Translation of the word in Vietnamese] | |
Q: [Original phrase with the target word in <b> tags, or craft an example with ONLY the target word in <b> tags if no phrase is provided. The Q must contain the word in <b> tags.] | |
A: [Short explanation of the word's meaning in the context] | |
Example: | |
Word: "refused" | |
Phrase: "Hamas refused to join a new round of peace negotiations." | |
T: từ chối | |
Q: "Hamas <b>refused</b> to join a new round of peace negotiations." | |
A: Declined to accept or comply with a request or proposal. | |
Example when no phrase is provided or it's unclear: | |
Word: "analogues" | |
Phrase: "" | |
T: tương tự | |
Q: "Scientists often use animal <b>analogues</b> to study human diseases." | |
A: Things or concepts that are similar or comparable to something else, often used in scientific contexts. | |
Now explain the word in the phrase below: | |
Word: "{word}" | |
Phrase: "{phrase}"</textarea> | |
</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> | |
<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> | |
<button id="submit-btn" style="display: block;">Generate</button> | |
<div id="flashcards"></div> | |
<div id="collection"> | |
<button id="add-to-collection-btn">Add to Collection (0)</button> | |
<button id="clear-collection-btn">Clear Collection</button> | |
</div> | |
<button id="export-csv-btn" style="display: none;">Export Flashcards to CSV</button> | |
<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 src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"></script> | |
<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 = 'claude-3-haiku-20240307'; | |
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 generateLanguageFlashcard(word, phrase, targetLanguage) { | |
if (!apiKey) { | |
alert('Please enter your Claude API key first.'); | |
return; | |
} | |
const prompt = document.getElementById('language-prompt').value | |
.replace('{word}', word) | |
.replace('{phrase}', phrase) | |
.replace('{targetLanguage}', targetLanguage); | |
try { | |
const response = await callClaudeAPI(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 Claude API:', error); | |
alert('Failed to generate language flashcard. Please check your API key and try again.'); | |
} | |
} | |
async function generateContent() { | |
if (!apiKey) { | |
alert('Please enter your Claude API key first.'); | |
return; | |
} | |
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, change its color, and show notification | |
submitBtn.disabled = true; | |
submitBtn.style.backgroundColor = '#808080'; // Change to gray | |
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)'; // Change to green | |
notification.style.color = 'white'; | |
notification.style.borderRadius = '5px'; | |
notification.style.zIndex = '1000'; | |
document.body.appendChild(notification); | |
try { | |
const response = await callClaudeAPI(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 Claude API:', error); | |
alert(`Failed to generate ${mode === 'flashcard' ? 'flashcards' : 'explanation'}. Please check your API key and try again.`); | |
} finally { | |
// Remove notification, re-enable button, and restore its color after 3 seconds | |
setTimeout(() => { | |
document.body.removeChild(notification); | |
submitBtn.disabled = false; | |
submitBtn.style.backgroundColor = ''; // Restore original color | |
}, 3000); | |
} | |
} else { | |
alert(`Please select some text from the PDF 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'; | |
} | |
} | |
} | |
async function callClaudeAPI(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 // Add the current mode to the request | |
}) | |
}); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
return await response.json(); | |
} | |
modelSelect.addEventListener('change', function () { | |
selectedModel = this.value; | |
}); | |
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(); | |
}); | |
flashcardsContainer.appendChild(flashcardElement); | |
}); | |
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(); | |
}); | |
flashcardsContainer.appendChild(flashcardElement); | |
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() { | |
const addToCollectionBtn = document.getElementById('add-to-collection-btn'); | |
const count = mode === 'language' ? languageCollectionCount : flashcardCollectionCount; | |
addToCollectionBtn.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-btn').addEventListener('click', addToCollection); | |
function updateExportButtonVisibility() { | |
const exportButton = document.getElementById('export-csv-btn'); | |
const currentCollection = mode === 'language' ? collectedLanguageFlashcards : collectedFlashcards; | |
exportButton.style.display = currentCollection.length > 0 ? 'block' : 'none'; | |
} | |
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); | |
} | |
document.getElementById('export-csv-btn').addEventListener('click', exportToCSV); | |
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-btn').addEventListener('click', 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 = mode === 'language' ? 'none' : 'block'; | |
submitBtn.textContent = mode === 'flashcard' ? 'Generate Flashcards' : 'Generate Explanation'; | |
if (mode === 'language') { | |
const savedLanguage = loadLanguageChoice(); | |
setLanguageButton(savedLanguage); | |
} | |
// Update Add to Collection button 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 hidden | |
document.getElementById('language-buttons').style.display = 'flex'; | |
submitBtn.style.display = 'none'; | |
// 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), | |
rect: { | |
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', 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'; | |
}); | |
// 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); | |
}); | |
} | |
</script> | |
</body> | |
</html> | |