diff --git a/web/src/components/pdf-previewer/index.less b/web/src/components/pdf-previewer/index.less new file mode 100644 index 000000000..b65199342 --- /dev/null +++ b/web/src/components/pdf-previewer/index.less @@ -0,0 +1,12 @@ +.documentContainer { + width: 100%; + height: 100%; + position: relative; + :global(.PdfHighlighter) { + overflow-x: hidden; + } + :global(.Highlight--scrolledTo .Highlight__part) { + overflow-x: hidden; + background-color: rgba(255, 226, 143, 1); + } +} diff --git a/web/src/components/pdf-previewer/index.tsx b/web/src/components/pdf-previewer/index.tsx new file mode 100644 index 000000000..e6f72ef1e --- /dev/null +++ b/web/src/components/pdf-previewer/index.tsx @@ -0,0 +1,116 @@ +import { + useGetChunkHighlights, + useGetDocumentUrl, +} from '@/hooks/documentHooks'; +import { IChunk } from '@/interfaces/database/knowledge'; +import { Skeleton } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import { + AreaHighlight, + Highlight, + IHighlight, + PdfHighlighter, + PdfLoader, + Popup, +} from 'react-pdf-highlighter'; + +import styles from './index.less'; + +interface IProps { + chunk: IChunk; + documentId: string; + visible: boolean; +} + +const HighlightPopup = ({ + comment, +}: { + comment: { text: string; emoji: string }; +}) => + comment.text ? ( +
+ {comment.emoji} {comment.text} +
+ ) : null; + +const DocumentPreviewer = ({ chunk, documentId, visible }: IProps) => { + const url = useGetDocumentUrl(documentId); + const state = useGetChunkHighlights(chunk); + const ref = useRef<(highlight: IHighlight) => void>(() => {}); + const [loaded, setLoaded] = useState(false); + + const resetHash = () => {}; + + useEffect(() => { + setLoaded(visible); + }, [visible]); + + useEffect(() => { + if (state.length > 0 && loaded) { + setLoaded(false); + ref.current(state[0]); + } + }, [state, loaded]); + + return ( +
+ }> + {(pdfDocument) => ( + event.altKey} + onScrollChange={resetHash} + scrollRef={(scrollTo) => { + ref.current = scrollTo; + setLoaded(true); + }} + onSelectionFinished={() => null} + highlightTransform={( + highlight, + index, + setTip, + hideTip, + viewportToScaled, + screenshot, + isScrolledTo, + ) => { + const isTextHighlight = !Boolean( + highlight.content && highlight.content.image, + ); + + const component = isTextHighlight ? ( + + ) : ( + {}} + /> + ); + + return ( + } + onMouseOver={(popupContent) => + setTip(highlight, () => popupContent) + } + onMouseOut={hideTip} + key={index} + > + {component} + + ); + }} + highlights={state} + /> + )} + +
+ ); +}; + +export default DocumentPreviewer; diff --git a/web/src/hooks/documentHooks.ts b/web/src/hooks/documentHooks.ts new file mode 100644 index 000000000..a79f9d16a --- /dev/null +++ b/web/src/hooks/documentHooks.ts @@ -0,0 +1,21 @@ +import { IChunk } from '@/interfaces/database/knowledge'; +import { api_host } from '@/utils/api'; +import { buildChunkHighlights } from '@/utils/documentUtils'; +import { useMemo } from 'react'; +import { IHighlight } from 'react-pdf-highlighter'; + +export const useGetDocumentUrl = (documentId: string) => { + const url = useMemo(() => { + return `${api_host}/document/get/${documentId}`; + }, [documentId]); + + return url; +}; + +export const useGetChunkHighlights = (selectedChunk: IChunk): IHighlight[] => { + const highlights: IHighlight[] = useMemo(() => { + return buildChunkHighlights(selectedChunk); + }, [selectedChunk]); + + return highlights; +}; diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index af6b12c90..76b590912 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -1,4 +1,5 @@ import { MessageType } from '@/constants/chat'; +import { IChunk } from './knowledge'; export interface PromptConfig { empty_response: string; @@ -66,7 +67,7 @@ export interface Message { } export interface IReference { - chunks: Chunk[]; + chunks: IChunk[]; doc_aggs: Docagg[]; total: number; } @@ -77,16 +78,16 @@ export interface Docagg { doc_name: string; } -interface Chunk { - chunk_id: string; - content_ltks: string; - content_with_weight: string; - doc_id: string; - docnm_kwd: string; - img_id: string; - important_kwd: any[]; - kb_id: string; - similarity: number; - term_similarity: number; - vector_similarity: number; -} +// interface Chunk { +// chunk_id: string; +// content_ltks: string; +// content_with_weight: string; +// doc_id: string; +// docnm_kwd: string; +// img_id: string; +// important_kwd: any[]; +// kb_id: string; +// similarity: number; +// term_similarity: number; +// vector_similarity: number; +// } 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 a6e0646e2..11283f2c7 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,8 +1,6 @@ .documentContainer { width: 100%; height: calc(100vh - 284px); - // overflow-y: auto; - // overflow-x: hidden; position: relative; :global(.PdfHighlighter) { overflow-x: hidden; 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 index e5c26c23f..a7444fe91 100644 --- 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 @@ -16,7 +16,6 @@ import styles from './index.less'; interface IProps { selectedChunkId: string; } - const HighlightPopup = ({ comment, }: { @@ -28,6 +27,7 @@ const HighlightPopup = ({ ) : null; +// TODO: merge with DocumentPreviewer const Preview = ({ selectedChunkId }: IProps) => { const url = useGetDocumentUrl(); const state = useGetChunkHighlights(selectedChunkId); diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts index aad1dd083..edd256b24 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts @@ -1,8 +1,8 @@ import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; +import { buildChunkHighlights } from '@/utils/documentUtils'; import { useCallback, useMemo, useState } from 'react'; import { IHighlight } from 'react-pdf-highlighter'; import { useSelector } from 'umi'; -import { v4 as uuid } from 'uuid'; export const useSelectDocumentInfo = () => { const documentInfo: IKnowledgeFile = useSelector( @@ -41,35 +41,7 @@ export const useGetChunkHighlights = ( const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId); const highlights: IHighlight[] = useMemo(() => { - return Array.isArray(selectedChunk?.positions) && - selectedChunk.positions.every((x) => Array.isArray(x)) - ? selectedChunk?.positions?.map((x) => { - const actualPositions = x.map((y, index) => - index !== 0 ? y / 0.7 : y, - ); - const boundingRect = { - width: 849, - height: 1200, - x1: actualPositions[1], - x2: actualPositions[2], - y1: actualPositions[3], - y2: actualPositions[4], - }; - return { - id: uuid(), - comment: { - text: '', - emoji: '', - }, - content: { text: selectedChunk.content_with_weight }, - position: { - boundingRect: boundingRect, - rects: [boundingRect], - pageNumber: x[0], - }, - }; - }) - : []; + return buildChunkHighlights(selectedChunk); }, [selectedChunk]); return highlights; diff --git a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.less b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.less index 62dc54208..25ccfaaa2 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.less +++ b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.less @@ -8,6 +8,8 @@ .popoverContentText { white-space: pre-line; + max-height: 50vh; + overflow: auto; .popoverContentErrorLabel { color: red; } diff --git a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx index a6053f01c..6f216551c 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx @@ -21,6 +21,25 @@ interface IProps { } const PopoverContent = ({ record }: IProps) => { + const replaceText = (text: string) => { + // Remove duplicate \n + const nextText = text.replace(/(\n)\1+/g, '$1'); + + const replacedText = reactStringReplace( + nextText, + /(\[ERROR\].+\s)/g, + (match, i) => { + return ( + + {match} + + ); + }, + ); + + return replacedText; + }; + const items: DescriptionsProps['items'] = [ { key: 'process_begin_at', @@ -35,17 +54,7 @@ const PopoverContent = ({ record }: IProps) => { { key: 'progress_msg', label: 'Progress Msg', - children: reactStringReplace( - record.progress_msg.trim(), - /(\[ERROR\].+\s)/g, - (match, i) => { - return ( - - {match} - - ); - }, - ), + children: replaceText(record.progress_msg.trim()), }, ]; diff --git a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx index c2e9c4a96..65c7daee5 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -65,7 +65,11 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => { > - + { const MessageItem = ({ item, reference, + clickDocumentButton, }: { item: Message; reference: IReference; + clickDocumentButton: (documentId: string, chunk: IChunk) => void; }) => { const userInfo = useSelectUserInfo(); const fileThumbnails = useSelectFileThumbnails(); const isAssistant = item.role === MessageType.Assistant; + const handleDocumentButtonClick = useCallback( + (documentId: string, chunk: IChunk) => () => { + clickDocumentButton(documentId, chunk); + }, + [clickDocumentButton], + ); + const getPopoverContent = useCallback( (chunkIndex: number) => { const chunks = reference?.chunks ?? []; @@ -83,16 +104,19 @@ const MessageItem = ({ {documentId && ( - + )} ); }, - [reference, fileThumbnails], + [reference, fileThumbnails, handleDocumentButtonClick], ); const renderReference = useCallback( @@ -191,6 +215,8 @@ const ChatContainer = () => { addNewestConversation, } = useFetchConversationOnMount(); const { sendMessage } = useSendMessage(); + const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = + useClickDrawer(); const loading = useOneNamespaceEffectsLoading('chatModel', [ 'completeConversation', @@ -210,41 +236,56 @@ const ChatContainer = () => { }; return ( - - -
- {conversation?.message?.map((message) => { - const assistantMessages = conversation?.message - ?.filter((x) => x.role === MessageType.Assistant) - .slice(1); - const referenceIndex = assistantMessages.findIndex( - (x) => x.id === message.id, - ); - const reference = conversation.reference[referenceIndex]; - return ( - - ); - })} -
-
+ <> + + +
+ {conversation?.message?.map((message) => { + const assistantMessages = conversation?.message + ?.filter((x) => x.role === MessageType.Assistant) + .slice(1); + const referenceIndex = assistantMessages.findIndex( + (x) => x.id === message.id, + ); + const reference = conversation.reference[referenceIndex]; + return ( + + ); + })} +
+
+ + + Send + + } + onPressEnter={handlePressEnter} + onChange={handleInputChange} + /> - - Send - - } - onPressEnter={handlePressEnter} - onChange={handleInputChange} - /> - + + + + ); }; diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 52e1553e2..1c8926be5 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -4,6 +4,7 @@ import { fileIconMap } from '@/constants/common'; import { useSetModalState } from '@/hooks/commonHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { IConversation, IDialog } from '@/interfaces/database/chat'; +import { IChunk } from '@/interfaces/database/knowledge'; import { getFileExtension } from '@/utils'; import omit from 'lodash/omit'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -662,4 +663,28 @@ export const useRenameConversation = () => { }; }; +export const useClickDrawer = () => { + const { visible, showModal, hideModal } = useSetModalState(); + const [selectedChunk, setSelectedChunk] = useState({} as IChunk); + const [documentId, setDocumentId] = useState(''); + + const clickDocumentButton = useCallback( + (documentId: string, chunk: IChunk) => { + showModal(); + setSelectedChunk(chunk); + setDocumentId(documentId); + }, + [showModal], + ); + + return { + clickDocumentButton, + visible, + showModal, + hideModal, + selectedChunk, + documentId, + }; +}; + //#endregion diff --git a/web/src/pages/knowledge/index.tsx b/web/src/pages/knowledge/index.tsx index 76ad3713e..78f2af55d 100644 --- a/web/src/pages/knowledge/index.tsx +++ b/web/src/pages/knowledge/index.tsx @@ -50,13 +50,7 @@ const Knowledge = () => {
- + {list.length > 0 ? ( list.map((item: any) => { return ; diff --git a/web/src/utils/documentUtils.ts b/web/src/utils/documentUtils.ts new file mode 100644 index 000000000..a2fb1e072 --- /dev/null +++ b/web/src/utils/documentUtils.ts @@ -0,0 +1,34 @@ +import { IChunk } from '@/interfaces/database/knowledge'; +import { v4 as uuid } from 'uuid'; + +export const buildChunkHighlights = (selectedChunk: IChunk) => { + return Array.isArray(selectedChunk?.positions) && + selectedChunk.positions.every((x) => Array.isArray(x)) + ? selectedChunk?.positions?.map((x) => { + const actualPositions = x.map((y, index) => + index !== 0 ? y / 0.7 : y, + ); + const boundingRect = { + width: 849, + height: 1200, + x1: actualPositions[1], + x2: actualPositions[2], + y1: actualPositions[3], + y2: actualPositions[4], + }; + return { + id: uuid(), + comment: { + text: '', + emoji: '', + }, + content: { text: selectedChunk.content_with_weight }, + position: { + boundingRect: boundingRect, + rects: [boundingRect], + pageNumber: x[0], + }, + }; + }) + : []; +};