Spaces:
Sleeping
Sleeping
Commit ·
e715c5d
1
Parent(s): 58e23bb
feat: show all pages with jump-to-mention navigation
Browse files- documents/route.js now includes ALL pages, not just those with datasets
- Added pages_with_mentions array to document data
- PageNavigator has ⏮/⏭ jump buttons to skip to pages with mentions
- Green ● dot indicator on pages that have data mentions
- Users can browse full document context while quickly jumping to mentions
- app/api/documents/route.js +6 -3
- app/components/PageNavigator.js +43 -16
- app/globals.css +31 -1
- app/page.js +26 -0
app/api/documents/route.js
CHANGED
|
@@ -106,11 +106,13 @@ export async function GET(request) {
|
|
| 106 |
if (!docRes.ok) return null;
|
| 107 |
|
| 108 |
const pagesData = await docRes.json();
|
| 109 |
-
const
|
|
|
|
|
|
|
| 110 |
.filter(page => page.datasets && page.datasets.length > 0)
|
| 111 |
.map(page => page.document.pages[0]);
|
| 112 |
|
| 113 |
-
if (
|
| 114 |
|
| 115 |
const pdfUrl = link.direct_pdf_url;
|
| 116 |
if (!pdfUrl) return null;
|
|
@@ -121,7 +123,8 @@ export async function GET(request) {
|
|
| 121 |
index: link.index,
|
| 122 |
pdf_url: pdfUrl,
|
| 123 |
landing_page: link.landing_page_url,
|
| 124 |
-
annotatable_pages:
|
|
|
|
| 125 |
};
|
| 126 |
})
|
| 127 |
);
|
|
|
|
| 106 |
if (!docRes.ok) return null;
|
| 107 |
|
| 108 |
const pagesData = await docRes.json();
|
| 109 |
+
const allPages = pagesData
|
| 110 |
+
.map(page => page.document.pages[0]);
|
| 111 |
+
const pagesWithMentions = pagesData
|
| 112 |
.filter(page => page.datasets && page.datasets.length > 0)
|
| 113 |
.map(page => page.document.pages[0]);
|
| 114 |
|
| 115 |
+
if (allPages.length === 0) return null;
|
| 116 |
|
| 117 |
const pdfUrl = link.direct_pdf_url;
|
| 118 |
if (!pdfUrl) return null;
|
|
|
|
| 123 |
index: link.index,
|
| 124 |
pdf_url: pdfUrl,
|
| 125 |
landing_page: link.landing_page_url,
|
| 126 |
+
annotatable_pages: allPages,
|
| 127 |
+
pages_with_mentions: pagesWithMentions
|
| 128 |
};
|
| 129 |
})
|
| 130 |
);
|
app/components/PageNavigator.js
CHANGED
|
@@ -4,33 +4,60 @@ export default function PageNavigator({
|
|
| 4 |
currentIndex,
|
| 5 |
totalPages,
|
| 6 |
currentPageNumber,
|
|
|
|
| 7 |
onPrevious,
|
| 8 |
onNext,
|
|
|
|
|
|
|
|
|
|
| 9 |
}) {
|
| 10 |
return (
|
| 11 |
<div className="page-navigator">
|
| 12 |
-
<
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
<span className="page-indicator">
|
| 22 |
Page <strong>{currentPageNumber}</strong>
|
|
|
|
| 23 |
<span className="page-count">{currentIndex + 1} / {totalPages}</span>
|
| 24 |
</span>
|
| 25 |
|
| 26 |
-
<
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
</div>
|
| 35 |
);
|
| 36 |
}
|
|
|
|
| 4 |
currentIndex,
|
| 5 |
totalPages,
|
| 6 |
currentPageNumber,
|
| 7 |
+
hasMentions,
|
| 8 |
onPrevious,
|
| 9 |
onNext,
|
| 10 |
+
onJumpToMention,
|
| 11 |
+
hasNextMention,
|
| 12 |
+
hasPrevMention,
|
| 13 |
}) {
|
| 14 |
return (
|
| 15 |
<div className="page-navigator">
|
| 16 |
+
<div className="page-nav-group">
|
| 17 |
+
<button
|
| 18 |
+
className="btn btn-nav btn-jump"
|
| 19 |
+
onClick={() => onJumpToMention(-1)}
|
| 20 |
+
disabled={!hasPrevMention}
|
| 21 |
+
aria-label="Previous page with mentions"
|
| 22 |
+
title="Jump to previous page with data mentions"
|
| 23 |
+
>
|
| 24 |
+
⏮
|
| 25 |
+
</button>
|
| 26 |
+
<button
|
| 27 |
+
className="btn btn-nav"
|
| 28 |
+
onClick={onPrevious}
|
| 29 |
+
disabled={currentIndex <= 0}
|
| 30 |
+
aria-label="Previous page"
|
| 31 |
+
>
|
| 32 |
+
← Prev
|
| 33 |
+
</button>
|
| 34 |
+
</div>
|
| 35 |
|
| 36 |
<span className="page-indicator">
|
| 37 |
Page <strong>{currentPageNumber}</strong>
|
| 38 |
+
{hasMentions && <span className="mention-dot" title="This page has data mentions">●</span>}
|
| 39 |
<span className="page-count">{currentIndex + 1} / {totalPages}</span>
|
| 40 |
</span>
|
| 41 |
|
| 42 |
+
<div className="page-nav-group">
|
| 43 |
+
<button
|
| 44 |
+
className="btn btn-nav"
|
| 45 |
+
onClick={onNext}
|
| 46 |
+
disabled={currentIndex >= totalPages - 1}
|
| 47 |
+
aria-label="Next page"
|
| 48 |
+
>
|
| 49 |
+
Next →
|
| 50 |
+
</button>
|
| 51 |
+
<button
|
| 52 |
+
className="btn btn-nav btn-jump"
|
| 53 |
+
onClick={() => onJumpToMention(1)}
|
| 54 |
+
disabled={!hasNextMention}
|
| 55 |
+
aria-label="Next page with mentions"
|
| 56 |
+
title="Jump to next page with data mentions"
|
| 57 |
+
>
|
| 58 |
+
⏭
|
| 59 |
+
</button>
|
| 60 |
+
</div>
|
| 61 |
</div>
|
| 62 |
);
|
| 63 |
}
|
app/globals.css
CHANGED
|
@@ -527,7 +527,12 @@ h4 {
|
|
| 527 |
.page-navigator {
|
| 528 |
display: flex;
|
| 529 |
align-items: center;
|
| 530 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
}
|
| 532 |
|
| 533 |
.page-indicator {
|
|
@@ -544,6 +549,26 @@ h4 {
|
|
| 544 |
color: #f8fafc;
|
| 545 |
}
|
| 546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
.page-count {
|
| 548 |
font-size: 0.75rem;
|
| 549 |
color: #64748b;
|
|
@@ -560,6 +585,11 @@ h4 {
|
|
| 560 |
transition: all 0.2s;
|
| 561 |
}
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
.btn-nav:hover:not(:disabled) {
|
| 564 |
background-color: var(--accent);
|
| 565 |
border-color: var(--accent);
|
|
|
|
| 527 |
.page-navigator {
|
| 528 |
display: flex;
|
| 529 |
align-items: center;
|
| 530 |
+
gap: 16px;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.page-nav-group {
|
| 534 |
+
display: flex;
|
| 535 |
+
gap: 4px;
|
| 536 |
}
|
| 537 |
|
| 538 |
.page-indicator {
|
|
|
|
| 549 |
color: #f8fafc;
|
| 550 |
}
|
| 551 |
|
| 552 |
+
.mention-dot {
|
| 553 |
+
color: #10b981;
|
| 554 |
+
font-size: 0.6rem;
|
| 555 |
+
margin-left: 4px;
|
| 556 |
+
vertical-align: middle;
|
| 557 |
+
animation: pulse-dot 2s ease-in-out infinite;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
@keyframes pulse-dot {
|
| 561 |
+
|
| 562 |
+
0%,
|
| 563 |
+
100% {
|
| 564 |
+
opacity: 1;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
50% {
|
| 568 |
+
opacity: 0.4;
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
.page-count {
|
| 573 |
font-size: 0.75rem;
|
| 574 |
color: #64748b;
|
|
|
|
| 585 |
transition: all 0.2s;
|
| 586 |
}
|
| 587 |
|
| 588 |
+
.btn-nav.btn-jump {
|
| 589 |
+
padding: 10px 12px;
|
| 590 |
+
font-size: 0.8rem;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
.btn-nav:hover:not(:disabled) {
|
| 594 |
background-color: var(--accent);
|
| 595 |
border-color: var(--accent);
|
app/page.js
CHANGED
|
@@ -47,6 +47,7 @@ export default function Home() {
|
|
| 47 |
|
| 48 |
// Derived: current page number from the annotatable_pages array
|
| 49 |
const annotatablePages = currentDoc?.annotatable_pages ?? [];
|
|
|
|
| 50 |
const currentPageNumber = annotatablePages[pageIdx] ?? null;
|
| 51 |
|
| 52 |
// Load documents (re-fetches when annotatorName changes to get user-specific assignment)
|
|
@@ -204,6 +205,27 @@ export default function Home() {
|
|
| 204 |
setPageIdx(prev => Math.min(annotatablePages.length - 1, prev + 1));
|
| 205 |
};
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
const handleAnnotate = (text, domOffset) => {
|
| 208 |
setSelectedText(text);
|
| 209 |
setSelectionOffset(domOffset || 0);
|
|
@@ -527,8 +549,12 @@ export default function Home() {
|
|
| 527 |
currentIndex={pageIdx}
|
| 528 |
totalPages={annotatablePages.length}
|
| 529 |
currentPageNumber={currentPageNumber}
|
|
|
|
| 530 |
onPrevious={handlePrevPage}
|
| 531 |
onNext={handleNextPage}
|
|
|
|
|
|
|
|
|
|
| 532 |
/>
|
| 533 |
</div>
|
| 534 |
|
|
|
|
| 47 |
|
| 48 |
// Derived: current page number from the annotatable_pages array
|
| 49 |
const annotatablePages = currentDoc?.annotatable_pages ?? [];
|
| 50 |
+
const pagesWithMentions = new Set(currentDoc?.pages_with_mentions ?? []);
|
| 51 |
const currentPageNumber = annotatablePages[pageIdx] ?? null;
|
| 52 |
|
| 53 |
// Load documents (re-fetches when annotatorName changes to get user-specific assignment)
|
|
|
|
| 205 |
setPageIdx(prev => Math.min(annotatablePages.length - 1, prev + 1));
|
| 206 |
};
|
| 207 |
|
| 208 |
+
const handleJumpToMention = (direction = 1) => {
|
| 209 |
+
const currentPage = annotatablePages[pageIdx];
|
| 210 |
+
if (direction === 1) {
|
| 211 |
+
// Find next page index (after current) that has mentions
|
| 212 |
+
for (let i = pageIdx + 1; i < annotatablePages.length; i++) {
|
| 213 |
+
if (pagesWithMentions.has(annotatablePages[i])) {
|
| 214 |
+
setPageIdx(i);
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
} else {
|
| 219 |
+
// Find previous page index that has mentions
|
| 220 |
+
for (let i = pageIdx - 1; i >= 0; i--) {
|
| 221 |
+
if (pagesWithMentions.has(annotatablePages[i])) {
|
| 222 |
+
setPageIdx(i);
|
| 223 |
+
return;
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
const handleAnnotate = (text, domOffset) => {
|
| 230 |
setSelectedText(text);
|
| 231 |
setSelectionOffset(domOffset || 0);
|
|
|
|
| 549 |
currentIndex={pageIdx}
|
| 550 |
totalPages={annotatablePages.length}
|
| 551 |
currentPageNumber={currentPageNumber}
|
| 552 |
+
hasMentions={pagesWithMentions.has(currentPageNumber)}
|
| 553 |
onPrevious={handlePrevPage}
|
| 554 |
onNext={handleNextPage}
|
| 555 |
+
onJumpToMention={handleJumpToMention}
|
| 556 |
+
hasNextMention={annotatablePages.slice(pageIdx + 1).some(p => pagesWithMentions.has(p))}
|
| 557 |
+
hasPrevMention={annotatablePages.slice(0, pageIdx).some(p => pagesWithMentions.has(p))}
|
| 558 |
/>
|
| 559 |
</div>
|
| 560 |
|