From 07d76ea18dcea63e8ae6af80fd585f39bb509633 Mon Sep 17 00:00:00 2001 From: balibabu <cike8899@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:30:28 +0800 Subject: [PATCH] feat: add DocumentPreviewer for chunk of chat reference and remove duplicate \n from record.progress_msg (#97) * feat: Remove duplicate \n from record.progress_msg * feat: add DocumentPreviewer for chunk of chat reference --- web/src/components/pdf-previewer/index.less | 12 ++ web/src/components/pdf-previewer/index.tsx | 116 +++++++++++++++++ web/src/hooks/documentHooks.ts | 21 ++++ web/src/interfaces/database/chat.ts | 29 ++--- .../components/document-preview/index.less | 2 - .../components/document-preview/preview.tsx | 2 +- .../components/knowledge-chunk/hooks.ts | 32 +---- .../parsing-status-cell/index.less | 2 + .../parsing-status-cell/index.tsx | 31 +++-- .../assistant-setting.tsx | 6 +- web/src/pages/chat/chat-container/index.tsx | 117 ++++++++++++------ web/src/pages/chat/hooks.ts | 25 ++++ web/src/pages/knowledge/index.tsx | 8 +- web/src/utils/documentUtils.ts | 34 +++++ 14 files changed, 333 insertions(+), 104 deletions(-) create mode 100644 web/src/components/pdf-previewer/index.less create mode 100644 web/src/components/pdf-previewer/index.tsx create mode 100644 web/src/hooks/documentHooks.ts create mode 100644 web/src/utils/documentUtils.ts diff --git a/web/src/components/pdf-previewer/index.less b/web/src/components/pdf-previewer/index.less new file mode 100644 index 0000000..b651993 --- /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 0000000..e6f72ef --- /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 ? ( + <div className="Highlight__popup"> + {comment.emoji} {comment.text} + </div> + ) : 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 ( + <div className={styles.documentContainer}> + <PdfLoader url={url} beforeLoad={<Skeleton active />}> + {(pdfDocument) => ( + <PdfHighlighter + pdfDocument={pdfDocument} + enableAreaSelection={(event) => 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 ? ( + <Highlight + isScrolledTo={isScrolledTo} + position={highlight.position} + comment={highlight.comment} + /> + ) : ( + <AreaHighlight + isScrolledTo={isScrolledTo} + highlight={highlight} + onChange={() => {}} + /> + ); + + return ( + <Popup + popupContent={<HighlightPopup {...highlight} />} + onMouseOver={(popupContent) => + setTip(highlight, () => popupContent) + } + onMouseOut={hideTip} + key={index} + > + {component} + </Popup> + ); + }} + highlights={state} + /> + )} + </PdfLoader> + </div> + ); +}; + +export default DocumentPreviewer; diff --git a/web/src/hooks/documentHooks.ts b/web/src/hooks/documentHooks.ts new file mode 100644 index 0000000..a79f9d1 --- /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 af6b12c..76b5909 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 a6e0646..11283f2 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 e5c26c2..a7444fe 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 = ({ </div> ) : 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 aad1dd0..edd256b 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 62dc542..25ccfaa 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 a6053f0..6f21655 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 ( + <span key={i} className={styles.popoverContentErrorLabel}> + {match} + </span> + ); + }, + ); + + 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 ( - <span key={i} className={styles.popoverContentErrorLabel}> - {match} - </span> - ); - }, - ), + 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 c2e9c4a..65c7dae 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) => { > <Input placeholder="" /> </Form.Item> - <Form.Item name={['prompt_config', 'prologue']} label="Set an opener"> + <Form.Item + name={['prompt_config', 'prologue']} + label="Set an opener" + initialValue={"Hi! I'm your assistant, what can I do for you?"} + > <Input.TextArea autoSize={{ minRows: 5 }} /> </Form.Item> <Form.Item diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx index 1019e64..39855cb 100644 --- a/web/src/pages/chat/chat-container/index.tsx +++ b/web/src/pages/chat/chat-container/index.tsx @@ -3,11 +3,21 @@ import { MessageType } from '@/constants/chat'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { useSelectUserInfo } from '@/hooks/userSettingHook'; import { IReference, Message } from '@/interfaces/database/chat'; -import { Avatar, Button, Flex, Input, List, Popover, Space } from 'antd'; +import { + Avatar, + Button, + Drawer, + Flex, + Input, + List, + Popover, + Space, +} from 'antd'; import classNames from 'classnames'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import reactStringReplace from 'react-string-replace'; import { + useClickDrawer, useFetchConversationOnMount, useGetFileIcon, useSendMessage, @@ -15,7 +25,9 @@ import { import Image from '@/components/image'; import NewDocumentLink from '@/components/new-document-link'; +import DocumentPreviewer from '@/components/pdf-previewer'; import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; +import { IChunk } from '@/interfaces/database/knowledge'; import { InfoCircleOutlined } from '@ant-design/icons'; import Markdown from 'react-markdown'; import { visitParents } from 'unist-util-visit-parents'; @@ -41,15 +53,24 @@ const rehypeWrapReference = () => { 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 && ( <Flex gap={'middle'}> <img src={fileThumbnails[documentId]} alt="" /> - <NewDocumentLink documentId={documentId}> + <Button + type="link" + onClick={handleDocumentButtonClick(documentId, chunkItem)} + > {document?.doc_name} - </NewDocumentLink> + </Button> </Flex> )} </Space> </Flex> ); }, - [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 ( - <Flex flex={1} className={styles.chatContainer} vertical> - <Flex flex={1} vertical className={styles.messageContainer}> - <div> - {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 ( - <MessageItem - key={message.id} - item={message} - reference={reference} - ></MessageItem> - ); - })} - </div> - <div ref={ref} /> + <> + <Flex flex={1} className={styles.chatContainer} vertical> + <Flex flex={1} vertical className={styles.messageContainer}> + <div> + {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 ( + <MessageItem + key={message.id} + item={message} + reference={reference} + clickDocumentButton={clickDocumentButton} + ></MessageItem> + ); + })} + </div> + <div ref={ref} /> + </Flex> + <Input + size="large" + placeholder="Message Resume Assistant..." + value={value} + suffix={ + <Button type="primary" onClick={handlePressEnter} loading={loading}> + Send + </Button> + } + onPressEnter={handlePressEnter} + onChange={handleInputChange} + /> </Flex> - <Input - size="large" - placeholder="Message Resume Assistant..." - value={value} - suffix={ - <Button type="primary" onClick={handlePressEnter} loading={loading}> - Send - </Button> - } - onPressEnter={handlePressEnter} - onChange={handleInputChange} - /> - </Flex> + <Drawer + title="Document Previewer" + onClose={hideModal} + open={visible} + width={'50vw'} + > + <DocumentPreviewer + documentId={documentId} + chunk={selectedChunk} + visible={visible} + ></DocumentPreviewer> + </Drawer> + </> ); }; diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 52e1553..1c8926b 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<IChunk>({} as IChunk); + const [documentId, setDocumentId] = useState<string>(''); + + 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 76ad371..78f2af5 100644 --- a/web/src/pages/knowledge/index.tsx +++ b/web/src/pages/knowledge/index.tsx @@ -50,13 +50,7 @@ const Knowledge = () => { </ModalManager> </Space> </div> - <Flex - gap="large" - wrap="wrap" - flex={1} - // justify="center" - className={styles.knowledgeCardContainer} - > + <Flex gap={'large'} wrap="wrap" className={styles.knowledgeCardContainer}> {list.length > 0 ? ( list.map((item: any) => { return <KnowledgeCard item={item} key={item.name}></KnowledgeCard>; diff --git a/web/src/utils/documentUtils.ts b/web/src/utils/documentUtils.ts new file mode 100644 index 0000000..a2fb1e0 --- /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], + }, + }; + }) + : []; +}; -- GitLab