diff --git a/package.json b/package.json index c956f56fe..38ed1d2bd 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "postprocessing": "^6.37.8", "react-day-picker": "^9.13.0", "react-markdown": "^10.1.0", + "react-pdf": "^10.4.1", "react-resizable-panels": "^3.0.4", "react-router-dom": "^7.6.0", "remark-gfm": "^4.0.1", diff --git a/src/components/Folder/PdfViewer.tsx b/src/components/Folder/PdfViewer.tsx new file mode 100644 index 000000000..f37b9b216 --- /dev/null +++ b/src/components/Folder/PdfViewer.tsx @@ -0,0 +1,133 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { Document, Page, pdfjs } from 'react-pdf'; +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import 'react-pdf/dist/Page/TextLayer.css'; + +// Configure PDF.js worker in the same module where we use Document/Page +pdfjs.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); + +const DEFAULT_PAGE_HEIGHT = 1056; + +interface LazyPageProps { + pageNumber: number; + width: number | undefined; +} + +const LazyPage = memo(function LazyPage({ pageNumber, width }: LazyPageProps) { + const [visible, setVisible] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const node = ref.current; + if (!node) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + observer.disconnect(); + } + }, + { rootMargin: '200px' } + ); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return ( +
+ {visible ? ( + + ) : ( +
+ )} +
+ ); +}); + +interface PdfViewerProps { + /** data URL (data:application/pdf;base64,...) or file path */ + content: string; +} + +export default function PdfViewer({ content }: PdfViewerProps) { + const [numPages, setNumPages] = useState(0); + const [containerWidth, setContainerWidth] = useState(0); + const [loadError, setLoadError] = useState(null); + const containerRef = useRef(null); + + const onDocumentLoadSuccess = useCallback( + ({ numPages }: { numPages: number }) => { + setNumPages(numPages); + setLoadError(null); + }, + [] + ); + + const onDocumentLoadError = useCallback((error: Error) => { + console.error('Failed to load PDF document:', error); + setLoadError(error.message ?? 'Failed to load PDF.'); + }, []); + + useEffect(() => { + const node = containerRef.current; + if (!node) return; + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return ( +
+ {loadError ? ( +
+

Failed to load PDF

+

+ {loadError} +

+
+ ) : ( + + {Array.from({ length: numPages }, (_, index) => ( + + ))} + + )} +
+ ); +} diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx index 3bbcb0971..65e296b52 100644 --- a/src/components/Folder/index.tsx +++ b/src/components/Folder/index.tsx @@ -30,9 +30,11 @@ import { Search, SquareTerminal, } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; +import { lazy, Suspense, useEffect, useRef, useState } from 'react'; import FolderComponent from './FolderComponent'; +const PdfViewer = lazy(() => import('./PdfViewer')); + import { proxyFetchGet } from '@/api/http'; import { MarkDown } from '@/components/ChatBox/MessageItem/MarkDown'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; @@ -709,12 +711,12 @@ export default function Folder({ data: _data }: { data?: Agent }) { className={`flex min-h-0 flex-1 flex-col ${selectedFile?.type === 'html' && !isShowSourceCode ? 'overflow-hidden' : 'scrollbar overflow-y-auto'}`} >
{selectedFile ? ( !loading ? ( selectedFile.type === 'md' && !isShowSourceCode ? ( -
+
) : selectedFile.type === 'pdf' ? ( -