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(); | |
}); | |
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() { | |
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 = mode === 'language' ? 'none' : 'block'; | |
// The settings panel will not autoβopen now. Users must open it manually via the settings icon. | |
// Update the collection button text and export button visibility as before | |
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'; | |
}); | |
// 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 = 'none'; | |
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> | |