Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

index.tsx 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import Image from '@/components/image';
  2. import SvgIcon from '@/components/svg-icon';
  3. import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
  4. import { getExtension } from '@/utils/document-util';
  5. import { InfoCircleOutlined } from '@ant-design/icons';
  6. import { Button, Flex, Popover, Space } from 'antd';
  7. import DOMPurify from 'dompurify';
  8. import { useCallback, useEffect, useMemo } from 'react';
  9. import Markdown from 'react-markdown';
  10. import reactStringReplace from 'react-string-replace';
  11. import SyntaxHighlighter from 'react-syntax-highlighter';
  12. import rehypeKatex from 'rehype-katex';
  13. import rehypeRaw from 'rehype-raw';
  14. import remarkGfm from 'remark-gfm';
  15. import remarkMath from 'remark-math';
  16. import { visitParents } from 'unist-util-visit-parents';
  17. import { useFetchDocumentThumbnailsByIds } from '@/hooks/document-hooks';
  18. import { useTranslation } from 'react-i18next';
  19. import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
  20. import { preprocessLaTeX, replaceThinkToSection } from '@/utils/chat';
  21. import { replaceTextByOldReg } from '../utils';
  22. import { pipe } from 'lodash/fp';
  23. import styles from './index.less';
  24. const reg = /(~{2}\d+={2})/g;
  25. const curReg = /(~{2}\d+\${2})/g;
  26. const getChunkIndex = (match: string) => Number(match.slice(2, -2));
  27. // TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
  28. const MarkdownContent = ({
  29. reference,
  30. clickDocumentButton,
  31. content,
  32. loading,
  33. }: {
  34. content: string;
  35. loading: boolean;
  36. reference: IReference;
  37. clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
  38. }) => {
  39. const { t } = useTranslation();
  40. const { setDocumentIds, data: fileThumbnails } =
  41. useFetchDocumentThumbnailsByIds();
  42. const contentWithCursor = useMemo(() => {
  43. let text = content;
  44. if (text === '') {
  45. text = t('chat.searching');
  46. }
  47. const nextText = replaceTextByOldReg(text);
  48. return loading
  49. ? nextText?.concat('~~2$$')
  50. : pipe(replaceThinkToSection, preprocessLaTeX)(nextText);
  51. }, [content, loading, t]);
  52. useEffect(() => {
  53. const docAggs = reference?.doc_aggs;
  54. setDocumentIds(Array.isArray(docAggs) ? docAggs.map((x) => x.doc_id) : []);
  55. }, [reference, setDocumentIds]);
  56. const handleDocumentButtonClick = useCallback(
  57. (documentId: string, chunk: IReferenceChunk, isPdf: boolean) => () => {
  58. if (!isPdf) {
  59. return;
  60. }
  61. clickDocumentButton?.(documentId, chunk);
  62. },
  63. [clickDocumentButton],
  64. );
  65. const rehypeWrapReference = () => {
  66. return function wrapTextTransform(tree: any) {
  67. visitParents(tree, 'text', (node, ancestors) => {
  68. const latestAncestor = ancestors.at(-1);
  69. if (
  70. latestAncestor.tagName !== 'custom-typography' &&
  71. latestAncestor.tagName !== 'code'
  72. ) {
  73. node.type = 'element';
  74. node.tagName = 'custom-typography';
  75. node.properties = {};
  76. node.children = [{ type: 'text', value: node.value }];
  77. }
  78. });
  79. };
  80. };
  81. const getPopoverContent = useCallback(
  82. (chunkIndex: number) => {
  83. const chunks = reference?.chunks ?? [];
  84. const chunkItem = chunks[chunkIndex];
  85. const document = reference?.doc_aggs?.find(
  86. (x) => x?.doc_id === chunkItem?.document_id,
  87. );
  88. const documentId = document?.doc_id;
  89. const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
  90. const fileExtension = documentId ? getExtension(document?.doc_name) : '';
  91. const imageId = chunkItem?.image_id;
  92. return (
  93. <Flex
  94. key={chunkItem?.id}
  95. gap={10}
  96. className={styles.referencePopoverWrapper}
  97. >
  98. {imageId && (
  99. <Popover
  100. placement="left"
  101. content={
  102. <Image
  103. id={imageId}
  104. className={styles.referenceImagePreview}
  105. ></Image>
  106. }
  107. >
  108. <Image
  109. id={imageId}
  110. className={styles.referenceChunkImage}
  111. ></Image>
  112. </Popover>
  113. )}
  114. <Space direction={'vertical'}>
  115. <div
  116. dangerouslySetInnerHTML={{
  117. __html: DOMPurify.sanitize(chunkItem?.content ?? ''),
  118. }}
  119. className={styles.chunkContentText}
  120. ></div>
  121. {documentId && (
  122. <Flex gap={'small'}>
  123. {fileThumbnail ? (
  124. <img
  125. src={fileThumbnail}
  126. alt=""
  127. className={styles.fileThumbnail}
  128. />
  129. ) : (
  130. <SvgIcon
  131. name={`file-icon/${fileExtension}`}
  132. width={24}
  133. ></SvgIcon>
  134. )}
  135. <Button
  136. type="link"
  137. className={styles.documentLink}
  138. onClick={handleDocumentButtonClick(
  139. documentId,
  140. chunkItem,
  141. fileExtension === 'pdf',
  142. )}
  143. >
  144. {document?.doc_name}
  145. </Button>
  146. </Flex>
  147. )}
  148. </Space>
  149. </Flex>
  150. );
  151. },
  152. [reference, fileThumbnails, handleDocumentButtonClick],
  153. );
  154. const renderReference = useCallback(
  155. (text: string) => {
  156. let replacedText = reactStringReplace(text, reg, (match, i) => {
  157. const chunkIndex = getChunkIndex(match);
  158. return (
  159. <Popover content={getPopoverContent(chunkIndex)} key={i}>
  160. <InfoCircleOutlined className={styles.referenceIcon} />
  161. </Popover>
  162. );
  163. });
  164. replacedText = reactStringReplace(replacedText, curReg, (match, i) => (
  165. <span className={styles.cursor} key={i}></span>
  166. ));
  167. return replacedText;
  168. },
  169. [getPopoverContent],
  170. );
  171. return (
  172. <Markdown
  173. rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]}
  174. remarkPlugins={[remarkGfm, remarkMath]}
  175. components={
  176. {
  177. 'custom-typography': ({ children }: { children: string }) =>
  178. renderReference(children),
  179. code(props: any) {
  180. const { children, className, node, ...rest } = props;
  181. const match = /language-(\w+)/.exec(className || '');
  182. return match ? (
  183. <SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
  184. {String(children).replace(/\n$/, '')}
  185. </SyntaxHighlighter>
  186. ) : (
  187. <code {...rest} className={className}>
  188. {children}
  189. </code>
  190. );
  191. },
  192. } as any
  193. }
  194. >
  195. {contentWithCursor}
  196. </Markdown>
  197. );
  198. };
  199. export default MarkdownContent;