From 1dccbb715297cda97aa68f4d527c49098702f03f Mon Sep 17 00:00:00 2001 From: Damir Dulic Date: Tue, 17 Mar 2026 22:18:54 +0000 Subject: [PATCH] feat: improve file explorer page numbering and insights UI - Fix page numbering to be 1-based (was 0-based) in the file viewer - Sync OCR sidebar scroll position to the currently visible page using IntersectionObserver - Filter OCR entries from the AI insights tab so it shows only AI summaries --- pyproject.toml | 2 +- supernote/server/services/file.py | 4 +- .../server/static/js/components/FileViewer.js | 46 +++++++++++++++++-- .../static/js/components/SummaryPanel.js | 27 +++++++++-- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d99041b..e50748c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"] [project] name = "supernote" -version = "1.2.0" +version = "1.3.0" license = "Apache-2.0" license-files = ["LICENSE"] description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services" diff --git a/supernote/server/services/file.py b/supernote/server/services/file.py index 058312c..08a0eca 100644 --- a/supernote/server/services/file.py +++ b/supernote/server/services/file.py @@ -976,7 +976,9 @@ async def convert_note_to_png(self, user: str, file_id: int) -> list[Conversions ) any_new_pages = True - results.append(ConversionsVO(storage_key=png_storage_key, page_no=i)) + results.append( + ConversionsVO(storage_key=png_storage_key, page_no=i + 1) + ) # Persist the MD5 used for this conversion so future calls can clean up stale images. if any_new_pages or node.last_conversion_md5 != current_md5: diff --git a/supernote/server/static/js/components/FileViewer.js b/supernote/server/static/js/components/FileViewer.js index 64088a2..620a40b 100644 --- a/supernote/server/static/js/components/FileViewer.js +++ b/supernote/server/static/js/components/FileViewer.js @@ -1,4 +1,4 @@ -import { ref, computed, watch, onMounted } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'; +import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'; import { convertNoteToPng, fetchStaleness, reprocessFile, reprocessPage } from '../api/client.js'; import SummaryPanel from './SummaryPanel.js'; @@ -18,6 +18,8 @@ export default { const isLoading = ref(false); const error = ref(null); const showDetails = ref(false); + const activePage = ref(1); + const scrollContainerRef = ref(null); // Staleness state const stalenessData = ref(null); // full response from /staleness @@ -114,6 +116,40 @@ export default { } } + // IntersectionObserver: track which page is most visible in the scroll area + let pageObserver = null; + const pageVisibility = new Map(); // pageNo -> intersectionRatio + + function setupPageObserver() { + if (pageObserver) { pageObserver.disconnect(); pageObserver = null; } + if (!scrollContainerRef.value || !pages.value.length) return; + + pageObserver = new IntersectionObserver((records) => { + for (const record of records) { + const pageNo = parseInt(record.target.dataset.pageNo); + pageVisibility.set(pageNo, record.intersectionRatio); + } + let best = activePage.value, bestRatio = -1; + for (const [pageNo, ratio] of pageVisibility) { + if (ratio > bestRatio) { bestRatio = ratio; best = pageNo; } + } + if (bestRatio > 0) activePage.value = best; + }, { root: scrollContainerRef.value, threshold: [0, 0.25, 0.5, 0.75, 1.0] }); + + for (const page of pages.value) { + const el = scrollContainerRef.value.querySelector(`[data-page-no="${page.pageNo}"]`); + if (el) pageObserver.observe(el); + } + } + + watch(pages, async () => { + pageVisibility.clear(); + await nextTick(); + setupPageObserver(); + }, { flush: 'post' }); + + onUnmounted(() => { if (pageObserver) pageObserver.disconnect(); }); + onMounted(loadPages); watch(() => props.file, loadPages); @@ -122,6 +158,8 @@ export default { isLoading, error, showDetails, + activePage, + scrollContainerRef, stalenessData, staleCount, reprocessingAll, @@ -170,7 +208,7 @@ export default {
-
+
@@ -189,7 +227,7 @@ export default {
-
+
Page {{ page.pageNo }} @@ -221,7 +259,7 @@ export default { leave-to-class="translate-x-full" >
- +
diff --git a/supernote/server/static/js/components/SummaryPanel.js b/supernote/server/static/js/components/SummaryPanel.js index 91c77e7..1ca8017 100644 --- a/supernote/server/static/js/components/SummaryPanel.js +++ b/supernote/server/static/js/components/SummaryPanel.js @@ -1,13 +1,18 @@ -import { ref, onMounted, watch } from 'vue'; +import { ref, onMounted, watch, nextTick } from 'vue'; import { fetchSummaries, fetchOcrPages } from '../api/client.js'; export default { props: { fileId: { required: true + }, + activePage: { + type: Number, + default: 1 } }, setup(props) { + const ocrContainerRef = ref(null); // AI tab state const summaries = ref([]); const isLoading = ref(false); @@ -30,7 +35,9 @@ export default { try { const result = await fetchSummaries(props.fileId); // Sort by creation time desc - summaries.value = result.sort((a, b) => (b.creationTime || 0) - (a.creationTime || 0)); + summaries.value = result + .filter(s => (s.dataSource || '').toUpperCase() !== 'OCR') + .sort((a, b) => (b.creationTime || 0) - (a.creationTime || 0)); } catch (e) { console.error(e); error.value = "Failed to load summaries."; @@ -61,6 +68,17 @@ export default { if (tab === 'ocr') loadOcr(); }; + function scrollOcrToPage(pageNo) { + if (!ocrContainerRef.value || !pageNo) return; + const el = ocrContainerRef.value.querySelector(`[data-ocr-page="${pageNo}"]`); + if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' }); + } + + watch([() => props.activePage, activeTab], ([newPage, tab]) => { + if (tab !== 'ocr') return; + nextTick(() => scrollOcrToPage(newPage)); + }); + onMounted(loadSummaries); watch(() => props.fileId, () => { // Reset all state when the viewed file changes @@ -90,6 +108,7 @@ export default { ocrPages, isOcrLoading, ocrError, + ocrContainerRef, selectTab, formatContent, formatDate @@ -163,7 +182,7 @@ export default {
-
+
@@ -182,7 +201,7 @@ export default {
-
+
Page {{ page.pageIndex + 1 }}