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.1KB

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