* feat: Remove duplicate \n from record.progress_msg * feat: add DocumentPreviewer for chunk of chat referencetags/v0.1.0
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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; | |||
| }; | |||
| @@ -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; | |||
| // } | |||
| @@ -1,8 +1,6 @@ | |||
| .documentContainer { | |||
| width: 100%; | |||
| height: calc(100vh - 284px); | |||
| // overflow-y: auto; | |||
| // overflow-x: hidden; | |||
| position: relative; | |||
| :global(.PdfHighlighter) { | |||
| overflow-x: hidden; | |||
| @@ -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); | |||
| @@ -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; | |||
| @@ -8,6 +8,8 @@ | |||
| .popoverContentText { | |||
| white-space: pre-line; | |||
| max-height: 50vh; | |||
| overflow: auto; | |||
| .popoverContentErrorLabel { | |||
| color: red; | |||
| } | |||
| @@ -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()), | |||
| }, | |||
| ]; | |||
| @@ -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 | |||
| @@ -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> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -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 | |||
| @@ -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>; | |||
| @@ -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], | |||
| }, | |||
| }; | |||
| }) | |||
| : []; | |||
| }; | |||