From 4fc76bc650d5d4c2c03da247bcc9e3d44973e48e Mon Sep 17 00:00:00 2001 From: ifedan-ed Date: Sat, 11 Apr 2026 04:56:30 +0200 Subject: [PATCH] Show PDF instantly from local file, extract text in background - PDF renders immediately on file select via blob URL (no server round-trip) - Text extraction happens in background (only needed for TTS audio) - Default view is now PDF (shows the real document, not extracted text) - User can switch to Text view to see extracted sentences with highlighting - Nextcloud PDFs also default to PDF view when opened Co-Authored-By: Claude Sonnet 4.6 --- static/app.js | 70 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/static/app.js b/static/app.js index db97918..5bf2726 100644 --- a/static/app.js +++ b/static/app.js @@ -279,15 +279,13 @@ async function loadDoc(text, title = 'Document', docId = null, startIdx = 0, pdf if (pdfB64) { viewToggle.style.display = ''; state.sentencePage = buildSentencePageMap(pageTexts, state.sentences); - // Render asynchronously so the text view appears immediately - renderPDF(pdfB64).catch(console.error); - // Default to text view - setView('text'); + renderPDF(pdfB64); + setView('pdf'); // default to real PDF view } else { viewToggle.style.display = 'none'; setView('text'); state.sentencePage = []; - pdfDoc = null; + if (pdfBlobUrl) { URL.revokeObjectURL(pdfBlobUrl); pdfBlobUrl = null; } $('pdf-pages').innerHTML = ''; } @@ -583,25 +581,67 @@ uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('drag uploadArea.addEventListener('drop', e => { e.preventDefault(); uploadArea.classList.remove('drag-over'); const f = e.dataTransfer.files[0]; - if (f?.type === 'application/pdf') uploadPDF(f); + if (f?.type === 'application/pdf') handlePDF(f); }); -pdfInput.addEventListener('change', () => { if (pdfInput.files[0]) uploadPDF(pdfInput.files[0]); }); +pdfInput.addEventListener('change', () => { if (pdfInput.files[0]) handlePDF(pdfInput.files[0]); }); -async function uploadPDF(file) { +async function handlePDF(file) { const status = $('pdf-status'); - showLoading('Extracting PDF…'); - const form = new FormData(); - form.append('file', file); + const title = file.name.replace(/\.pdf$/i, ''); + + // ── Step 1: show PDF instantly from local file, no server needed ── + if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl); + pdfBlobUrl = URL.createObjectURL(file); + + // Show header + PDF view right away + $('document-header').style.display = ''; + $('document-title').textContent = title; + $('document-meta').textContent = 'Extracting text for playback…'; + $('view-toggle').style.display = ''; + $('pdf-pages').innerHTML = ''; + const iframe = document.createElement('iframe'); + iframe.id = 'pdf-iframe'; + iframe.src = pdfBlobUrl; + iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;'; + $('pdf-pages').appendChild(iframe); + setView('pdf'); // ← show the real PDF immediately + + nowPlaying.textContent = 'Extracting text for playback…'; + status.textContent = 'Extracting…'; + + // ── Step 2: extract text in background (needed only for TTS) ── try { + const form = new FormData(); + form.append('file', file); const resp = await fetch('/api/extract-pdf', { method: 'POST', body: form }); if (!resp.ok) throw new Error((await resp.json()).detail || 'Failed'); const data = await resp.json(); - await loadDoc(data.text, data.title || file.name, null, 0, data.pdf_b64, data.page_texts || []); - status.textContent = '✓ Loaded'; + + // Update state with extracted sentences (keep PDF view active) + state.sentences = splitSentences(data.text); + state.docTitle = data.title || title; + state.currentIdx = 0; + state.docId = null; + state.pageTexts = data.page_texts || []; + state.sentencePage = buildSentencePageMap(state.pageTexts, state.sentences); + audioCache.clear(); + + const real = state.sentences.filter(s => !s.isPara).length; + $('document-meta').textContent = `${real} sentences · ready`; + $('document-title').textContent = data.title || title; + totSent.textContent = real; + curSent.textContent = 0; + progressFill.style.width = '0%'; + nowPlaying.textContent = 'Ready — press play'; + + // Also render text view in background for when user switches + renderText(state.sentences); + textViewer.style.fontSize = `${state.fontSize}px`; + + status.textContent = '✓ Ready'; } catch (e) { status.textContent = '✗ ' + e.message; - } finally { - hideLoading(); + nowPlaying.textContent = 'Text extraction failed'; } }