balibabu
Fix: Fixed an issue where math formulas could not be displayed correctly #4405 (#4506)
18e87af
| import Image from '@/components/image'; | |
| import SvgIcon from '@/components/svg-icon'; | |
| import { IReference, IReferenceChunk } from '@/interfaces/database/chat'; | |
| import { getExtension } from '@/utils/document-util'; | |
| import { InfoCircleOutlined } from '@ant-design/icons'; | |
| import { Button, Flex, Popover, Space } from 'antd'; | |
| import DOMPurify from 'dompurify'; | |
| import { useCallback, useEffect, useMemo } from 'react'; | |
| import Markdown from 'react-markdown'; | |
| import reactStringReplace from 'react-string-replace'; | |
| import SyntaxHighlighter from 'react-syntax-highlighter'; | |
| import rehypeKatex from 'rehype-katex'; | |
| import rehypeRaw from 'rehype-raw'; | |
| import remarkGfm from 'remark-gfm'; | |
| import remarkMath from 'remark-math'; | |
| import { visitParents } from 'unist-util-visit-parents'; | |
| import { useFetchDocumentThumbnailsByIds } from '@/hooks/document-hooks'; | |
| import { useTranslation } from 'react-i18next'; | |
| import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you | |
| import { preprocessLaTeX } from '@/utils/chat'; | |
| import { replaceTextByOldReg } from '../utils'; | |
| import styles from './index.less'; | |
| const reg = /(~{2}\d+={2})/g; | |
| const curReg = /(~{2}\d+\${2})/g; | |
| const getChunkIndex = (match: string) => Number(match.slice(2, -2)); | |
| // TODO: The display of the table is inconsistent with the display previously placed in the MessageItem. | |
| const MarkdownContent = ({ | |
| reference, | |
| clickDocumentButton, | |
| content, | |
| loading, | |
| }: { | |
| content: string; | |
| loading: boolean; | |
| reference: IReference; | |
| clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void; | |
| }) => { | |
| const { t } = useTranslation(); | |
| const { setDocumentIds, data: fileThumbnails } = | |
| useFetchDocumentThumbnailsByIds(); | |
| const contentWithCursor = useMemo(() => { | |
| let text = content; | |
| if (text === '') { | |
| text = t('chat.searching'); | |
| } | |
| const nextText = replaceTextByOldReg(text); | |
| return loading ? nextText?.concat('~~2$$') : preprocessLaTeX(nextText); | |
| }, [content, loading, t]); | |
| useEffect(() => { | |
| setDocumentIds(reference?.doc_aggs?.map((x) => x.doc_id) ?? []); | |
| }, [reference, setDocumentIds]); | |
| const handleDocumentButtonClick = useCallback( | |
| (documentId: string, chunk: IReferenceChunk, isPdf: boolean) => () => { | |
| if (!isPdf) { | |
| return; | |
| } | |
| clickDocumentButton?.(documentId, chunk); | |
| }, | |
| [clickDocumentButton], | |
| ); | |
| const rehypeWrapReference = () => { | |
| return function wrapTextTransform(tree: any) { | |
| visitParents(tree, 'text', (node, ancestors) => { | |
| const latestAncestor = ancestors.at(-1); | |
| if ( | |
| latestAncestor.tagName !== 'custom-typography' && | |
| latestAncestor.tagName !== 'code' | |
| ) { | |
| node.type = 'element'; | |
| node.tagName = 'custom-typography'; | |
| node.properties = {}; | |
| node.children = [{ type: 'text', value: node.value }]; | |
| } | |
| }); | |
| }; | |
| }; | |
| const getPopoverContent = useCallback( | |
| (chunkIndex: number) => { | |
| const chunks = reference?.chunks ?? []; | |
| const chunkItem = chunks[chunkIndex]; | |
| const document = reference?.doc_aggs?.find( | |
| (x) => x?.doc_id === chunkItem?.document_id, | |
| ); | |
| const documentId = document?.doc_id; | |
| const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; | |
| const fileExtension = documentId ? getExtension(document?.doc_name) : ''; | |
| const imageId = chunkItem?.image_id; | |
| return ( | |
| <Flex | |
| key={chunkItem?.id} | |
| gap={10} | |
| className={styles.referencePopoverWrapper} | |
| > | |
| {imageId && ( | |
| <Popover | |
| placement="left" | |
| content={ | |
| <Image | |
| id={imageId} | |
| className={styles.referenceImagePreview} | |
| ></Image> | |
| } | |
| > | |
| <Image | |
| id={imageId} | |
| className={styles.referenceChunkImage} | |
| ></Image> | |
| </Popover> | |
| )} | |
| <Space direction={'vertical'}> | |
| <div | |
| dangerouslySetInnerHTML={{ | |
| __html: DOMPurify.sanitize(chunkItem?.content ?? ''), | |
| }} | |
| className={styles.chunkContentText} | |
| ></div> | |
| {documentId && ( | |
| <Flex gap={'small'}> | |
| {fileThumbnail ? ( | |
| <img | |
| src={fileThumbnail} | |
| alt="" | |
| className={styles.fileThumbnail} | |
| /> | |
| ) : ( | |
| <SvgIcon | |
| name={`file-icon/${fileExtension}`} | |
| width={24} | |
| ></SvgIcon> | |
| )} | |
| <Button | |
| type="link" | |
| className={styles.documentLink} | |
| onClick={handleDocumentButtonClick( | |
| documentId, | |
| chunkItem, | |
| fileExtension === 'pdf', | |
| )} | |
| > | |
| {document?.doc_name} | |
| </Button> | |
| </Flex> | |
| )} | |
| </Space> | |
| </Flex> | |
| ); | |
| }, | |
| [reference, fileThumbnails, handleDocumentButtonClick], | |
| ); | |
| const renderReference = useCallback( | |
| (text: string) => { | |
| let replacedText = reactStringReplace(text, reg, (match, i) => { | |
| const chunkIndex = getChunkIndex(match); | |
| return ( | |
| <Popover content={getPopoverContent(chunkIndex)} key={i}> | |
| <InfoCircleOutlined className={styles.referenceIcon} /> | |
| </Popover> | |
| ); | |
| }); | |
| replacedText = reactStringReplace(replacedText, curReg, (match, i) => ( | |
| <span className={styles.cursor} key={i}></span> | |
| )); | |
| return replacedText; | |
| }, | |
| [getPopoverContent], | |
| ); | |
| return ( | |
| <Markdown | |
| rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]} | |
| remarkPlugins={[remarkGfm, remarkMath]} | |
| components={ | |
| { | |
| 'custom-typography': ({ children }: { children: string }) => | |
| renderReference(children), | |
| code(props: any) { | |
| const { children, className, node, ...rest } = props; | |
| const match = /language-(\w+)/.exec(className || ''); | |
| return match ? ( | |
| <SyntaxHighlighter {...rest} PreTag="div" language={match[1]}> | |
| {String(children).replace(/\n$/, '')} | |
| </SyntaxHighlighter> | |
| ) : ( | |
| <code {...rest} className={className}> | |
| {children} | |
| </code> | |
| ); | |
| }, | |
| } as any | |
| } | |
| > | |
| {contentWithCursor} | |
| </Markdown> | |
| ); | |
| }; | |
| export default MarkdownContent; | |