LE Quoc Dat
dark
a8a3527
raw
history blame
61.5 kB
<!DOCTYPE html>
<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">&times;</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>