Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

index.tsx 5.5KB

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