You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
  2. import { MessageType } from '@/constants/chat';
  3. import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
  4. import { useSelectUserInfo } from '@/hooks/userSettingHook';
  5. import { IReference, Message } from '@/interfaces/database/chat';
  6. import { Avatar, Button, Flex, Input, List, Popover, Space } from 'antd';
  7. import classNames from 'classnames';
  8. import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
  9. import reactStringReplace from 'react-string-replace';
  10. import {
  11. useFetchConversationOnMount,
  12. useGetFileIcon,
  13. useSendMessage,
  14. } from '../hooks';
  15. import Image from '@/components/image';
  16. import NewDocumentLink from '@/components/new-document-link';
  17. import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
  18. import { InfoCircleOutlined } from '@ant-design/icons';
  19. import Markdown from 'react-markdown';
  20. import { visitParents } from 'unist-util-visit-parents';
  21. import styles from './index.less';
  22. const reg = /(#{2}\d+\${2})/g;
  23. const getChunkIndex = (match: string) => Number(match.slice(2, 3));
  24. const rehypeWrapReference = () => {
  25. return function wrapTextTransform(tree: any) {
  26. visitParents(tree, 'text', (node, ancestors) => {
  27. if (ancestors.at(-1).tagName !== 'custom-typography') {
  28. node.type = 'element';
  29. node.tagName = 'custom-typography';
  30. node.properties = {};
  31. node.children = [{ type: 'text', value: node.value }];
  32. }
  33. });
  34. };
  35. };
  36. const MessageItem = ({
  37. item,
  38. reference,
  39. }: {
  40. item: Message;
  41. reference: IReference;
  42. }) => {
  43. const userInfo = useSelectUserInfo();
  44. const fileThumbnails = useSelectFileThumbnails();
  45. const isAssistant = item.role === MessageType.Assistant;
  46. const getPopoverContent = useCallback(
  47. (chunkIndex: number) => {
  48. const chunks = reference?.chunks ?? [];
  49. const chunkItem = chunks[chunkIndex];
  50. const document = reference?.doc_aggs.find(
  51. (x) => x?.doc_id === chunkItem?.doc_id,
  52. );
  53. const documentId = document?.doc_id;
  54. return (
  55. <Flex
  56. key={chunkItem?.chunk_id}
  57. gap={10}
  58. className={styles.referencePopoverWrapper}
  59. >
  60. <Popover
  61. placement="topRight"
  62. content={
  63. <Image
  64. id={chunkItem?.img_id}
  65. className={styles.referenceImagePreview}
  66. ></Image>
  67. }
  68. >
  69. <Image
  70. id={chunkItem?.img_id}
  71. className={styles.referenceChunkImage}
  72. ></Image>
  73. </Popover>
  74. <Space direction={'vertical'}>
  75. <div>{chunkItem?.content_with_weight}</div>
  76. {documentId && (
  77. <Flex gap={'middle'}>
  78. <img src={fileThumbnails[documentId]} alt="" />
  79. <NewDocumentLink documentId={documentId}>
  80. {document?.doc_name}
  81. </NewDocumentLink>
  82. </Flex>
  83. )}
  84. </Space>
  85. </Flex>
  86. );
  87. },
  88. [reference, fileThumbnails],
  89. );
  90. const renderReference = useCallback(
  91. (text: string) => {
  92. return reactStringReplace(text, reg, (match, i) => {
  93. const chunkIndex = getChunkIndex(match);
  94. return (
  95. <Popover content={getPopoverContent(chunkIndex)}>
  96. <InfoCircleOutlined key={i} className={styles.referenceIcon} />
  97. </Popover>
  98. );
  99. });
  100. },
  101. [getPopoverContent],
  102. );
  103. const referenceDocumentList = useMemo(() => {
  104. return reference?.doc_aggs ?? [];
  105. }, [reference?.doc_aggs]);
  106. return (
  107. <div
  108. className={classNames(styles.messageItem, {
  109. [styles.messageItemLeft]: item.role === MessageType.Assistant,
  110. [styles.messageItemRight]: item.role === MessageType.User,
  111. })}
  112. >
  113. <section
  114. className={classNames(styles.messageItemSection, {
  115. [styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
  116. [styles.messageItemSectionRight]: item.role === MessageType.User,
  117. })}
  118. >
  119. <div
  120. className={classNames(styles.messageItemContent, {
  121. [styles.messageItemContentReverse]: item.role === MessageType.User,
  122. })}
  123. >
  124. {item.role === MessageType.User ? (
  125. userInfo.avatar ?? (
  126. <Avatar
  127. size={40}
  128. src={
  129. userInfo.avatar ??
  130. 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
  131. }
  132. />
  133. )
  134. ) : (
  135. <AssistantIcon></AssistantIcon>
  136. )}
  137. <Flex vertical gap={8} flex={1}>
  138. <b>{isAssistant ? '' : userInfo.nickname}</b>
  139. <div className={styles.messageText}>
  140. <Markdown
  141. rehypePlugins={[rehypeWrapReference]}
  142. components={
  143. {
  144. 'custom-typography': ({ children }: { children: string }) =>
  145. renderReference(children),
  146. } as any
  147. }
  148. >
  149. {item.content}
  150. </Markdown>
  151. </div>
  152. {isAssistant && referenceDocumentList.length > 0 && (
  153. <List
  154. bordered
  155. dataSource={referenceDocumentList}
  156. renderItem={(item) => (
  157. <List.Item>
  158. {/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */}
  159. <Flex gap={'middle'}>
  160. <img src={fileThumbnails[item.doc_id]}></img>
  161. <NewDocumentLink documentId={item.doc_id}>
  162. {item.doc_name}
  163. </NewDocumentLink>
  164. </Flex>
  165. </List.Item>
  166. )}
  167. />
  168. )}
  169. </Flex>
  170. </div>
  171. </section>
  172. </div>
  173. );
  174. };
  175. const ChatContainer = () => {
  176. const [value, setValue] = useState('');
  177. const {
  178. ref,
  179. currentConversation: conversation,
  180. addNewestConversation,
  181. } = useFetchConversationOnMount();
  182. const { sendMessage } = useSendMessage();
  183. const loading = useOneNamespaceEffectsLoading('chatModel', [
  184. 'completeConversation',
  185. ]);
  186. useGetFileIcon();
  187. const handlePressEnter = () => {
  188. if (!loading) {
  189. setValue('');
  190. addNewestConversation(value);
  191. sendMessage(value);
  192. }
  193. };
  194. const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
  195. setValue(e.target.value);
  196. };
  197. return (
  198. <Flex flex={1} className={styles.chatContainer} vertical>
  199. <Flex flex={1} vertical className={styles.messageContainer}>
  200. <div>
  201. {conversation?.message?.map((message) => {
  202. const assistantMessages = conversation?.message
  203. ?.filter((x) => x.role === MessageType.Assistant)
  204. .slice(1);
  205. const referenceIndex = assistantMessages.findIndex(
  206. (x) => x.id === message.id,
  207. );
  208. const reference = conversation.reference[referenceIndex];
  209. return (
  210. <MessageItem
  211. key={message.id}
  212. item={message}
  213. reference={reference}
  214. ></MessageItem>
  215. );
  216. })}
  217. </div>
  218. <div ref={ref} />
  219. </Flex>
  220. <Input
  221. size="large"
  222. placeholder="Message Resume Assistant..."
  223. value={value}
  224. suffix={
  225. <Button type="primary" onClick={handlePressEnter} loading={loading}>
  226. Send
  227. </Button>
  228. }
  229. onPressEnter={handlePressEnter}
  230. onChange={handleInputChange}
  231. />
  232. </Flex>
  233. );
  234. };
  235. export default ChatContainer;