| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- 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, replaceThinkToSection } from '@/utils/chat';
- import { replaceTextByOldReg } from '../utils';
-
- import { pipe } from 'lodash/fp';
- 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$$')
- : pipe(replaceThinkToSection, preprocessLaTeX)(nextText);
- }, [content, loading, t]);
-
- useEffect(() => {
- const docAggs = reference?.doc_aggs;
- setDocumentIds(Array.isArray(docAggs) ? docAggs.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;
|