SokratesAI / frontend /src /components /DocumentViewer.jsx
Alleinzellgaenger's picture
Implement navigation via chunks
4a6c290
raw
history blame
12.7 kB
import { useState, useRef, useEffect } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
const DocumentViewer = ({ selectedFile, documentData, onPageChange }) => {
const pdfContainerRef = useRef(null);
const [numPages, setNumPages] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [zoomLevel, setZoomLevel] = useState(1);
const [visiblePages, setVisiblePages] = useState(new Set([1]));
const [containerWidth, setContainerWidth] = useState(0);
// Update container width on mount and resize
useEffect(() => {
const updateContainerWidth = () => {
if (pdfContainerRef.current) {
const width = pdfContainerRef.current.clientWidth;
// Subtract padding and some margin for optimal viewing
const availableWidth = width - 32; // 16px padding on each side
setContainerWidth(availableWidth);
}
};
updateContainerWidth();
window.addEventListener('resize', updateContainerWidth);
return () => window.removeEventListener('resize', updateContainerWidth);
}, []);
// Expose goToPage function to parent component (only once when component mounts)
useEffect(() => {
if (onPageChange) {
onPageChange({ goToPage });
}
}, [onPageChange]);
// Calculate optimal page width
const getPageWidth = () => {
if (!containerWidth) return 600; // Fallback
// Use container width minus some margin, but respect zoom level
const baseWidth = Math.min(containerWidth * 0.99, 2000); // Max 800px for readability
return baseWidth * zoomLevel;
};
// Handle scroll to update current page and track visible pages
const handleScroll = () => {
if (!pdfContainerRef.current || !numPages) return;
const container = pdfContainerRef.current;
const scrollTop = container.scrollTop;
const containerHeight = container.clientHeight;
const totalScrollHeight = container.scrollHeight - containerHeight;
// Calculate which page we're viewing based on scroll position
const scrollPercent = scrollTop / totalScrollHeight;
const newPage = Math.min(Math.floor(scrollPercent * numPages) + 1, numPages);
if (newPage !== currentPage) {
setCurrentPage(newPage);
}
// Track visible pages based on zoom level
const newVisiblePages = new Set();
const visibleRange = Math.max(1, Math.ceil(2 / zoomLevel));
for (let i = Math.max(1, newPage - visibleRange); i <= Math.min(numPages, newPage + visibleRange); i++) {
newVisiblePages.add(i);
}
// Update visible pages if changed
if (newVisiblePages.size !== visiblePages.size ||
![...newVisiblePages].every(page => visiblePages.has(page))) {
setVisiblePages(newVisiblePages);
}
};
// Jump to specific page
const goToPage = (pageNumber) => {
if (!pdfContainerRef.current || !numPages || pageNumber < 1 || pageNumber > numPages) return;
// Update visible pages immediately for target page
const newVisiblePages = new Set();
const visibleRange = Math.max(1, Math.ceil(2 / zoomLevel));
for (let i = Math.max(1, pageNumber - visibleRange); i <= Math.min(numPages, pageNumber + visibleRange); i++) {
newVisiblePages.add(i);
}
setVisiblePages(newVisiblePages);
// Use setTimeout to ensure pages are rendered before scrolling
setTimeout(() => {
const container = pdfContainerRef.current;
if (!container) return;
// Find the target page element by its data attribute or position
const pageElements = container.querySelectorAll('[data-page-number]');
let targetElement = null;
// If we can't find elements by data attribute, calculate position manually
if (pageElements.length === 0) {
// Calculate approximate position based on page height
// Each page has some margin (mb-4 = 16px) plus the actual page height
const containerHeight = container.clientHeight;
const totalContent = container.scrollHeight;
const avgPageHeight = totalContent / numPages;
const targetPosition = (pageNumber - 1) * avgPageHeight;
// Center the page in viewport
const scrollPosition = Math.max(0, targetPosition - containerHeight / 4);
container.scrollTo({
top: scrollPosition,
behavior: 'smooth'
});
} else {
// Find the specific page element
for (const element of pageElements) {
if (parseInt(element.getAttribute('data-page-number')) === pageNumber) {
targetElement = element;
break;
}
}
if (targetElement) {
// Scroll to center the page in viewport
const elementRect = targetElement.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const scrollOffset = container.scrollTop;
const targetPosition = scrollOffset + elementRect.top - containerRect.top - (container.clientHeight - elementRect.height) / 4;
container.scrollTo({
top: Math.max(0, targetPosition),
behavior: 'smooth'
});
}
}
}, 100); // Small delay to ensure rendering
};
// Zoom controls
const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 3));
const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5));
const resetZoom = () => setZoomLevel(1);
if (!selectedFile) {
return (
<div className="bg-white rounded-lg shadow-sm flex items-center justify-center h-full">
<div className="text-center text-gray-500">
<p>No PDF selected</p>
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-sm flex flex-col relative" style={{ width: '100%', height: '100%' }}>
<div className="sticky top-0 bg-white rounded-t-lg px-6 py-4 border-b border-gray-200 z-10">
<h2 className="text-lg font-semibold text-left text-gray-800">
{documentData?.filename || 'Document'}
</h2>
</div>
{/* PDF Container */}
<div
ref={pdfContainerRef}
className="flex-1 overflow-auto flex justify-center bg-gray-100"
onScroll={handleScroll}
>
<div className="py-4">
<Document
file={selectedFile}
onLoadSuccess={({ numPages }) => setNumPages(numPages)}
>
{/* Render all pages continuously */}
{numPages && Array.from(new Array(numPages), (_, index) => {
const pageNum = index + 1;
const isVisible = visiblePages.has(pageNum);
return (
<div key={pageNum} className="mb-4 flex justify-center" data-page-number={pageNum}>
<Page
pageNumber={pageNum}
width={isVisible ? getPageWidth() : getPageWidth() / zoomLevel}
/>
</div>
);
})}
</Document>
</div>
</div>
{/* Pagination overlay - floating pill */}
{numPages && (
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 z-10">
<div className="flex items-center bg-gray-800/90 backdrop-blur-sm rounded-full shadow-lg px-3 py-2 space-x-3">
<button
onClick={() => goToPage(Math.max(currentPage - 1, 1))}
disabled={currentPage <= 1}
className="w-8 h-8 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M10 12l-4-4 4-4v8z"/>
</svg>
</button>
<span className="px-3 py-1 text-sm font-medium text-white min-w-[60px] text-center">
{currentPage}/{numPages}
</span>
<button
onClick={() => goToPage(Math.min(currentPage + 1, numPages))}
disabled={currentPage >= numPages}
className="w-8 h-8 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M6 4l4 4-4 4V4z"/>
</svg>
</button>
</div>
</div>
)}
{/* Zoom controls overlay - bottom right */}
{numPages && (
<div className="absolute bottom-4 right-4 z-10 flex flex-col items-center space-y-2">
{/* Main zoom pill - vertical */}
<div className="flex flex-col items-center bg-gray-800/90 backdrop-blur-sm rounded-full shadow-lg px-2 py-2 space-y-1">
<button
onClick={zoomIn}
disabled={zoomLevel >= 3}
className="w-6 h-6 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4v4H4v1h4v4h1V9h4V8H9V4z"/>
</svg>
</button>
<button
onClick={zoomOut}
disabled={zoomLevel <= 0.5}
className="w-6 h-6 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 8h8v1H4z"/>
</svg>
</button>
</div>
{/* Reset button below */}
<button
onClick={resetZoom}
className="w-10 h-10 bg-gray-700 hover:bg-gray-500 backdrop-blur-sm rounded-full shadow-lg flex items-center justify-center text-white transition-colors"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" stroke="currentColor" strokeWidth="0.5">
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" strokeWidth="1"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
</button>
</div>
)}
</div>
);
};
export default DocumentViewer;