From 0469b631bec0b6448072353691efe670ebb526e2 Mon Sep 17 00:00:00 2001 From: billchen Date: Fri, 1 Mar 2024 18:47:12 +0800 Subject: [PATCH] feat: add Preview with react-pdf-highlighter --- web/package-lock.json | 98 ++++++++- web/package.json | 1 + .../document-preview/hightlights.ts | 33 +++ .../components/document-preview/hooks.ts | 16 +- .../components/document-preview/index.less | 9 +- .../components/document-preview/preview.tsx | 189 ++++++++++++++++++ .../components/knowledge-chunk/index.tsx | 3 +- 7 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hightlights.ts create mode 100644 web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 6b41aecbf..4f0a37bc6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -24,6 +24,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^9.0.1", "react-pdf": "^7.7.1", + "react-pdf-highlighter": "^6.1.0", "react-string-replace": "^1.1.1", "umi": "^4.0.90", "umi-request": "^1.4.0", @@ -7264,6 +7265,12 @@ "node": ">= 4" } }, + "node_modules/dommatrix": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/dommatrix/-/dommatrix-1.0.3.tgz", + "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", + "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix." + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz", @@ -8562,6 +8569,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "peer": true }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" + }, "node_modules/fast-redact": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/fast-redact/-/fast-redact-3.3.0.tgz", @@ -11209,8 +11221,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -14417,6 +14428,18 @@ "react-dom": "*" } }, + "node_modules/re-resizable": { + "version": "6.9.6", + "resolved": "https://registry.npmmirror.com/re-resizable/-/re-resizable-6.9.6.tgz", + "integrity": "sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==", + "dependencies": { + "fast-memoize": "^2.5.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", @@ -14736,6 +14759,27 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.4.5.tgz", + "integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmmirror.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz", @@ -14872,6 +14916,37 @@ } } }, + "node_modules/react-pdf-highlighter": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/react-pdf-highlighter/-/react-pdf-highlighter-6.1.0.tgz", + "integrity": "sha512-PD7l+0q1v+pZahLA/2AeWIb0n8d1amL6o+mOKnldIqtyChBHSE3gfnY5ZNMSFrhWXdlM6l4Eet+aydnYo6Skow==", + "dependencies": { + "lodash.debounce": "^4.0.8", + "pdfjs-dist": "2.16.105", + "react-rnd": "^10.1.10" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/react-pdf-highlighter/node_modules/pdfjs-dist": { + "version": "2.16.105", + "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz", + "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==", + "dependencies": { + "dommatrix": "^1.0.3", + "web-streams-polyfill": "^3.2.1" + }, + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.0.tgz", @@ -14880,6 +14955,25 @@ "node": ">=0.10.0" } }, + "node_modules/react-rnd": { + "version": "10.4.1", + "resolved": "https://registry.npmmirror.com/react-rnd/-/react-rnd-10.4.1.tgz", + "integrity": "sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q==", + "dependencies": { + "re-resizable": "6.9.6", + "react-draggable": "4.4.5", + "tslib": "2.3.1" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, "node_modules/react-router": { "version": "6.3.0", "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.3.0.tgz", diff --git a/web/package.json b/web/package.json index 688e6b1b4..c9687f891 100644 --- a/web/package.json +++ b/web/package.json @@ -28,6 +28,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^9.0.1", "react-pdf": "^7.7.1", + "react-pdf-highlighter": "^6.1.0", "react-string-replace": "^1.1.1", "umi": "^4.0.90", "umi-request": "^1.4.0", diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hightlights.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hightlights.ts new file mode 100644 index 000000000..f2c81d89e --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hightlights.ts @@ -0,0 +1,33 @@ +export const testHighlights = [ + { + content: { + text: '实验证明,由氧氯化锆锂和高镍三元正极组成的全固态锂电池展示了极为优异的性能:在12 分钟快速充电的条件下,该电池仍然成功地在室温稳定循环2000 圈以上。', + }, + position: { + boundingRect: { + x1: 219.7, + y1: 204.3, + x2: 547.0, + y2: 264.0, + width: 849, + height: 1200, + }, + rects: [ + { + x1: 219.7, + y1: 204.3, + x2: 547.0, + y2: 264.0, + width: 849, + height: 1200, + }, + ], + pageNumber: 9, + }, + comment: { + text: 'Flow or TypeScript?', + emoji: '🔥', + }, + id: '8245652131754351', + }, +]; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts index 41eeb452a..1d0917aed 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts @@ -1,6 +1,8 @@ +import { useGetKnowledgeSearchParams } from '@/hooks/knowledgeHook'; +import { api_host } from '@/utils/api'; import { useSize } from 'ahooks'; import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; export const useDocumentResizeObserver = () => { const [containerWidth, setContainerWidth] = useState(); @@ -21,8 +23,6 @@ export const useDocumentResizeObserver = () => { }; function highlightPattern(text: string, pattern: string, pageNumber: number) { - const finalText = ''; - console.info(text); if (pageNumber === 2) { return `${text}`; } @@ -43,3 +43,13 @@ export const useHighlightText = (searchText: string = '') => { return textRenderer; }; + +export const useGetDocumentUrl = () => { + const { documentId } = useGetKnowledgeSearchParams(); + + const url = useMemo(() => { + return `${api_host}/document/get/${documentId}`; + }, [documentId]); + + return url; +}; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/index.less b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/index.less index 69a26abf7..c9b34ff05 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/index.less +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/index.less @@ -1,6 +1,11 @@ .documentContainer { width: 100%; height: calc(100vh - 284px); - overflow-y: auto; - overflow-x: hidden; + // overflow-y: auto; + // overflow-x: hidden; + position: relative; + :global(.PdfHighlighter) { + overflow-x: hidden; + // left: 0; + } } diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx new file mode 100644 index 000000000..0c735438d --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx @@ -0,0 +1,189 @@ +import { Spin } from 'antd'; +import { useRef, useState } from 'react'; +import type { NewHighlight } from 'react-pdf-highlighter'; +import { + AreaHighlight, + Highlight, + PdfHighlighter, + PdfLoader, + Popup, + Tip, +} from 'react-pdf-highlighter'; +import { useGetSelectedChunk } from '../../hooks'; +import { testHighlights } from './hightlights'; +import { useGetDocumentUrl } from './hooks'; + +import styles from './index.less'; + +interface IProps { + selectedChunkId: string; +} + +const getNextId = () => String(Math.random()).slice(2); + +const HighlightPopup = ({ + comment, +}: { + comment: { text: string; emoji: string }; +}) => + comment.text ? ( +
+ {comment.emoji} {comment.text} +
+ ) : null; + +const Preview = ({ selectedChunkId }: IProps) => { + const url = useGetDocumentUrl(); + const selectedChunk = useGetSelectedChunk(selectedChunkId); + + const [state, setState] = useState(testHighlights); + const ref = useRef((highlight: any) => {}); + + const parseIdFromHash = () => + document.location.hash.slice('#highlight-'.length); + + const resetHash = () => { + document.location.hash = ''; + }; + + const getHighlightById = (id: string) => { + const highlights = state; + + return highlights.find((highlight: any) => highlight.id === id); + }; + + // let scrollViewerTo = (highlight: any) => {}; + + let scrollToHighlightFromHash = () => { + const highlight = getHighlightById(parseIdFromHash()); + + if (highlight) { + ref.current(highlight); + } + }; + + const addHighlight = (highlight: NewHighlight) => { + const highlights = state; + + console.log('Saving highlight', highlight); + + setState([{ ...highlight, id: getNextId() }, ...highlights]); + }; + + const updateHighlight = ( + highlightId: string, + position: Object, + content: Object, + ) => { + console.log('Updating highlight', highlightId, position, content); + + setState( + state.map((h: any) => { + const { + id, + position: originalPosition, + content: originalContent, + ...rest + } = h; + return id === highlightId + ? { + id, + position: { ...originalPosition, ...position }, + content: { ...originalContent, ...content }, + ...rest, + } + : h; + }), + ); + }; + + // useEffect(() => { + // ref.current(testHighlights[0]); + // }, [selectedChunk]); + + return ( +
+ }> + {(pdfDocument) => ( + event.altKey} + onScrollChange={resetHash} + // pdfScaleValue="page-width" + + scrollRef={(scrollTo) => { + // scrollViewerTo = scrollTo; + ref.current = scrollTo; + + scrollToHighlightFromHash(); + }} + onSelectionFinished={( + position, + content, + hideTipAndSelection, + transformSelection, + ) => ( + { + addHighlight({ content, position, comment }); + + hideTipAndSelection(); + }} + /> + )} + highlightTransform={( + highlight, + index, + setTip, + hideTip, + viewportToScaled, + screenshot, + isScrolledTo, + ) => { + const isTextHighlight = !Boolean( + highlight.content && highlight.content.image, + ); + + const component = isTextHighlight ? ( + + ) : ( + { + updateHighlight( + highlight.id, + { boundingRect: viewportToScaled(boundingRect) }, + { image: screenshot(boundingRect) }, + ); + }} + /> + ); + + return ( + } + onMouseOver={(popupContent) => + setTip(highlight, (highlight: any) => popupContent) + } + onMouseOut={hideTip} + key={index} + > + {component} + + ); + }} + highlights={state} + /> + )} + +
+ ); +}; + +export default Preview; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx index 907b7b64e..01873cec7 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx @@ -8,7 +8,8 @@ import CreatingModal from './components/chunk-creating-modal'; import { useDeleteChunkByIds } from '@/hooks/knowledgeHook'; import ChunkCard from './components/chunk-card'; import ChunkToolBar from './components/chunk-toolbar'; -import DocumentPreview from './components/document-preview'; +// import DocumentPreview from './components/document-preview'; +import DocumentPreview from './components/document-preview/preview'; import { useHandleChunkCardClick, useSelectDocumentInfo } from './hooks'; import styles from './index.less'; import { ChunkModelState } from './model';