Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion supernote/server/services/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
46 changes: 42 additions & 4 deletions supernote/server/static/js/components/FileViewer.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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
Expand Down Expand Up @@ -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);

Expand All @@ -122,6 +158,8 @@ export default {
isLoading,
error,
showDetails,
activePage,
scrollContainerRef,
stalenessData,
staleCount,
reprocessingAll,
Expand Down Expand Up @@ -170,7 +208,7 @@ export default {
<!-- Main Content Area -->
<div class="flex-1 overflow-hidden relative flex">
<!-- Pages (Scrollable) -->
<div class="flex-1 overflow-y-auto p-4 sm:p-8">
<div ref="scrollContainerRef" class="flex-1 overflow-y-auto p-4 sm:p-8">
<div class="max-w-4xl mx-auto">
<!-- Error State -->
<div v-if="error" class="bg-white dark:bg-gray-800 p-12 rounded border border-gray-200 dark:border-gray-700 text-center">
Expand All @@ -189,7 +227,7 @@ export default {

<!-- Pages List -->
<div v-if="!isLoading && !error && pages.length > 0" class="space-y-6">
<div v-for="page in pages" :key="page.pageNo" class="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 overflow-hidden">
<div v-for="page in pages" :key="page.pageNo" :data-page-no="page.pageNo" class="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="border-b border-gray-200 dark:border-gray-700 p-3 bg-gray-50 dark:bg-gray-700 flex justify-between items-center text-xs text-gray-400 font-mono">
<div class="flex items-center gap-2">
<span>Page {{ page.pageNo }}</span>
Expand Down Expand Up @@ -221,7 +259,7 @@ export default {
leave-to-class="translate-x-full"
>
<div v-if="showDetails" class="w-96 border-l border-gray-200 dark:border-gray-700 z-20 absolute right-0 top-0 bottom-0 bg-white dark:bg-gray-800 md:relative">
<summary-panel :file-id="file.id" @close="showDetails = false"></summary-panel>
<summary-panel :file-id="file.id" :active-page="activePage" @close="showDetails = false"></summary-panel>
</div>
</transition>
</div>
Expand Down
27 changes: 23 additions & 4 deletions supernote/server/static/js/components/SummaryPanel.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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.";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -90,6 +108,7 @@ export default {
ocrPages,
isOcrLoading,
ocrError,
ocrContainerRef,
selectTab,
formatContent,
formatDate
Expand Down Expand Up @@ -163,7 +182,7 @@ export default {
</div>

<!-- OCR Tab Content -->
<div v-if="activeTab === 'ocr'" class="flex-1 overflow-y-auto p-4 space-y-4">
<div v-if="activeTab === 'ocr'" ref="ocrContainerRef" class="flex-1 overflow-y-auto p-4 space-y-4">
<!-- Loading -->
<div v-if="isOcrLoading" class="flex flex-col items-center justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-black dark:border-white mb-3"></div>
Expand All @@ -182,7 +201,7 @@ export default {
</div>

<!-- Pages -->
<div v-for="page in ocrPages" :key="page.pageIndex" class="bg-gray-50 dark:bg-gray-700 rounded p-4 border border-gray-200 dark:border-gray-600">
<div v-for="page in ocrPages" :key="page.pageIndex" :data-ocr-page="page.pageIndex + 1" class="bg-gray-50 dark:bg-gray-700 rounded p-4 border border-gray-200 dark:border-gray-600">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-2 font-mono">
Page {{ page.pageIndex + 1 }}
</div>
Expand Down
Loading