mirror of
				https://github.com/infiniflow/ragflow.git
				synced 2025-10-31 01:40:20 +00:00 
			
		
		
		
	feat: Support for conversational streaming (#809)
### What problem does this PR solve? feat: Support for conversational streaming #709 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
		
							parent
							
								
									95f809187e
								
							
						
					
					
						commit
						c6c9dbde64
					
				
							
								
								
									
										9
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -15,6 +15,7 @@ | |||||||
|         "axios": "^1.6.3", |         "axios": "^1.6.3", | ||||||
|         "classnames": "^2.5.1", |         "classnames": "^2.5.1", | ||||||
|         "dayjs": "^1.11.10", |         "dayjs": "^1.11.10", | ||||||
|  |         "eventsource-parser": "^1.1.2", | ||||||
|         "i18next": "^23.7.16", |         "i18next": "^23.7.16", | ||||||
|         "js-base64": "^3.7.5", |         "js-base64": "^3.7.5", | ||||||
|         "jsencrypt": "^3.3.2", |         "jsencrypt": "^3.3.2", | ||||||
| @ -10206,6 +10207,14 @@ | |||||||
|         "node": ">=0.8.x" |         "node": ">=0.8.x" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/eventsource-parser": { | ||||||
|  |       "version": "1.1.2", | ||||||
|  |       "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz", | ||||||
|  |       "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14.18" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/evp_bytestokey": { |     "node_modules/evp_bytestokey": { | ||||||
|       "version": "1.0.3", |       "version": "1.0.3", | ||||||
|       "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", |       "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
|   "author": "zhaofengchao <13723060510@163.com>", |   "author": "zhaofengchao <13723060510@163.com>", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "build": "umi build", |     "build": "umi build", | ||||||
|     "dev": "cross-env PORT=9200 UMI_DEV_SERVER_COMPRESS=none umi dev", |     "dev": "cross-env UMI_DEV_SERVER_COMPRESS=none umi dev", | ||||||
|     "postinstall": "umi setup", |     "postinstall": "umi setup", | ||||||
|     "lint": "umi lint --eslint-only", |     "lint": "umi lint --eslint-only", | ||||||
|     "setup": "umi setup", |     "setup": "umi setup", | ||||||
| @ -19,6 +19,7 @@ | |||||||
|     "axios": "^1.6.3", |     "axios": "^1.6.3", | ||||||
|     "classnames": "^2.5.1", |     "classnames": "^2.5.1", | ||||||
|     "dayjs": "^1.11.10", |     "dayjs": "^1.11.10", | ||||||
|  |     "eventsource-parser": "^1.1.2", | ||||||
|     "i18next": "^23.7.16", |     "i18next": "^23.7.16", | ||||||
|     "js-base64": "^3.7.5", |     "js-base64": "^3.7.5", | ||||||
|     "jsencrypt": "^3.3.2", |     "jsencrypt": "^3.3.2", | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ const NewDocumentLink = ({ | |||||||
|       onClick={!preventDefault ? undefined : (e) => e.preventDefault()} |       onClick={!preventDefault ? undefined : (e) => e.preventDefault()} | ||||||
|       href={link} |       href={link} | ||||||
|       rel="noreferrer" |       rel="noreferrer" | ||||||
|       style={{ color }} |       style={{ color, wordBreak: 'break-all' }} | ||||||
|     > |     > | ||||||
|       {children} |       {children} | ||||||
|     </a> |     </a> | ||||||
|  | |||||||
| @ -154,6 +154,9 @@ export const useRemoveConversation = () => { | |||||||
|   return removeConversation; |   return removeConversation; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /* | ||||||
|  | @deprecated | ||||||
|  |  */ | ||||||
| export const useCompleteConversation = () => { | export const useCompleteConversation = () => { | ||||||
|   const dispatch = useDispatch(); |   const dispatch = useDispatch(); | ||||||
| 
 | 
 | ||||||
| @ -283,20 +286,4 @@ export const useFetchSharedConversation = () => { | |||||||
|   return fetchSharedConversation; |   return fetchSharedConversation; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const useCompleteSharedConversation = () => { |  | ||||||
|   const dispatch = useDispatch(); |  | ||||||
| 
 |  | ||||||
|   const completeSharedConversation = useCallback( |  | ||||||
|     (payload: any) => { |  | ||||||
|       return dispatch<any>({ |  | ||||||
|         type: 'chatModel/completeExternalConversation', |  | ||||||
|         payload: payload, |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     [dispatch], |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   return completeSharedConversation; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| //#endregion
 | //#endregion
 | ||||||
|  | |||||||
| @ -1,13 +1,14 @@ | |||||||
| import { Authorization } from '@/constants/authorization'; | import { Authorization } from '@/constants/authorization'; | ||||||
| import { LanguageTranslationMap } from '@/constants/common'; | import { LanguageTranslationMap } from '@/constants/common'; | ||||||
| import { Pagination } from '@/interfaces/common'; | import { Pagination } from '@/interfaces/common'; | ||||||
|  | import { IAnswer } from '@/interfaces/database/chat'; | ||||||
| import { IKnowledgeFile } from '@/interfaces/database/knowledge'; | import { IKnowledgeFile } from '@/interfaces/database/knowledge'; | ||||||
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | ||||||
| import api from '@/utils/api'; | import api from '@/utils/api'; | ||||||
| import authorizationUtil from '@/utils/authorizationUtil'; | import { getAuthorization } from '@/utils/authorizationUtil'; | ||||||
| import { getSearchValue } from '@/utils/commonUtil'; |  | ||||||
| import { PaginationProps } from 'antd'; | import { PaginationProps } from 'antd'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
|  | import { EventSourceParserStream } from 'eventsource-parser/stream'; | ||||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | import { useCallback, useEffect, useMemo, useState } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { useDispatch } from 'umi'; | import { useDispatch } from 'umi'; | ||||||
| @ -138,62 +139,60 @@ export const useFetchAppConf = () => { | |||||||
|   return appConf; |   return appConf; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const useConnectWithSse = (url: string) => { | export const useSendMessageWithSse = ( | ||||||
|   const [content, setContent] = useState<string>(''); |   url: string = api.completeConversation, | ||||||
|  | ) => { | ||||||
|  |   const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); | ||||||
|  |   const [done, setDone] = useState(true); | ||||||
| 
 | 
 | ||||||
|   const connect = useCallback(() => { |  | ||||||
|     const source = new EventSource( |  | ||||||
|       url || '/sse/createSseEmitter?clientId=123456', |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     source.onopen = function () { |  | ||||||
|       console.log('Connection to the server was opened.'); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     source.onmessage = function (event: any) { |  | ||||||
|       setContent(event.data); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     source.onerror = function (error) { |  | ||||||
|       console.error('Error occurred:', error); |  | ||||||
|     }; |  | ||||||
|   }, [url]); |  | ||||||
| 
 |  | ||||||
|   return { connect, content }; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const useConnectWithSseNext = () => { |  | ||||||
|   const [content, setContent] = useState<string>(''); |  | ||||||
|   const sharedId = getSearchValue('shared_id'); |  | ||||||
|   const authorization = sharedId |  | ||||||
|     ? 'Bearer ' + sharedId |  | ||||||
|     : authorizationUtil.getAuthorization(); |  | ||||||
|   const send = useCallback( |   const send = useCallback( | ||||||
|     async (body: any) => { |     async (body: any) => { | ||||||
|       const response = await fetch(api.completeConversation, { |       try { | ||||||
|  |         setDone(false); | ||||||
|  |         const response = await fetch(url, { | ||||||
|           method: 'POST', |           method: 'POST', | ||||||
|           headers: { |           headers: { | ||||||
|           [Authorization]: authorization, |             [Authorization]: getAuthorization(), | ||||||
|             'Content-Type': 'application/json', |             'Content-Type': 'application/json', | ||||||
|           }, |           }, | ||||||
|           body: JSON.stringify(body), |           body: JSON.stringify(body), | ||||||
|         }); |         }); | ||||||
|  | 
 | ||||||
|         const reader = response?.body |         const reader = response?.body | ||||||
|           ?.pipeThrough(new TextDecoderStream()) |           ?.pipeThrough(new TextDecoderStream()) | ||||||
|  |           .pipeThrough(new EventSourceParserStream()) | ||||||
|           .getReader(); |           .getReader(); | ||||||
| 
 | 
 | ||||||
|       // const reader = response.body.getReader();
 |  | ||||||
| 
 |  | ||||||
|         while (true) { |         while (true) { | ||||||
|         const { value, done } = await reader?.read(); |           const x = await reader?.read(); | ||||||
|         console.log('Received', value); |           if (x) { | ||||||
|         setContent(value); |             const { done, value } = x; | ||||||
|         if (done) break; |             try { | ||||||
|  |               const val = JSON.parse(value?.data || ''); | ||||||
|  |               const d = val?.data; | ||||||
|  |               if (typeof d !== 'boolean') { | ||||||
|  |                 console.info('data:', d); | ||||||
|  |                 setAnswer(d); | ||||||
|               } |               } | ||||||
|  |             } catch (e) { | ||||||
|  |               console.warn(e); | ||||||
|  |             } | ||||||
|  |             if (done) { | ||||||
|  |               console.info('done'); | ||||||
|  |               break; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         console.info('done?'); | ||||||
|  |         setDone(true); | ||||||
|         return response; |         return response; | ||||||
|  |       } catch (e) { | ||||||
|  |         setDone(true); | ||||||
|  |         console.warn(e); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     [authorization], |     [url], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return { send, content }; |   return { send, answer, done }; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -72,6 +72,11 @@ export interface IReference { | |||||||
|   total: number; |   total: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface IAnswer { | ||||||
|  |   answer: string; | ||||||
|  |   reference: IReference; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface Docagg { | export interface Docagg { | ||||||
|   count: number; |   count: number; | ||||||
|   doc_id: string; |   doc_id: string; | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ export default { | |||||||
|       comingSoon: 'Coming Soon', |       comingSoon: 'Coming Soon', | ||||||
|       download: 'Download', |       download: 'Download', | ||||||
|       close: 'Close', |       close: 'Close', | ||||||
|  |       preview: 'Preview', | ||||||
|     }, |     }, | ||||||
|     login: { |     login: { | ||||||
|       login: 'Sign in', |       login: 'Sign in', | ||||||
| @ -381,6 +382,7 @@ export default { | |||||||
|       partialTitle: 'Partial Embed', |       partialTitle: 'Partial Embed', | ||||||
|       extensionTitle: 'Chrome Extension', |       extensionTitle: 'Chrome Extension', | ||||||
|       tokenError: 'Please create API Token first!', |       tokenError: 'Please create API Token first!', | ||||||
|  |       searching: 'searching...', | ||||||
|     }, |     }, | ||||||
|     setting: { |     setting: { | ||||||
|       profile: 'Profile', |       profile: 'Profile', | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ export default { | |||||||
|       comingSoon: '即將推出', |       comingSoon: '即將推出', | ||||||
|       download: '下載', |       download: '下載', | ||||||
|       close: '关闭', |       close: '关闭', | ||||||
|  |       preview: '預覽', | ||||||
|     }, |     }, | ||||||
|     login: { |     login: { | ||||||
|       login: '登入', |       login: '登入', | ||||||
| @ -352,6 +353,7 @@ export default { | |||||||
|       partialTitle: '部分嵌入', |       partialTitle: '部分嵌入', | ||||||
|       extensionTitle: 'Chrome 插件', |       extensionTitle: 'Chrome 插件', | ||||||
|       tokenError: '請先創建 Api Token!', |       tokenError: '請先創建 Api Token!', | ||||||
|  |       searching: '搜索中', | ||||||
|     }, |     }, | ||||||
|     setting: { |     setting: { | ||||||
|       profile: '概述', |       profile: '概述', | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ export default { | |||||||
|       comingSoon: '即将推出', |       comingSoon: '即将推出', | ||||||
|       download: '下载', |       download: '下载', | ||||||
|       close: '关闭', |       close: '关闭', | ||||||
|  |       preview: '预览', | ||||||
|     }, |     }, | ||||||
|     login: { |     login: { | ||||||
|       login: '登录', |       login: '登录', | ||||||
| @ -369,6 +370,7 @@ export default { | |||||||
|       partialTitle: '部分嵌入', |       partialTitle: '部分嵌入', | ||||||
|       extensionTitle: 'Chrome 插件', |       extensionTitle: 'Chrome 插件', | ||||||
|       tokenError: '请先创建 Api Token!', |       tokenError: '请先创建 Api Token!', | ||||||
|  |       searching: '搜索中', | ||||||
|     }, |     }, | ||||||
|     setting: { |     setting: { | ||||||
|       profile: '概要', |       profile: '概要', | ||||||
|  | |||||||
| @ -6,16 +6,7 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; | |||||||
| import { useSelectUserInfo } from '@/hooks/userSettingHook'; | import { useSelectUserInfo } from '@/hooks/userSettingHook'; | ||||||
| import { IReference, Message } from '@/interfaces/database/chat'; | import { IReference, Message } from '@/interfaces/database/chat'; | ||||||
| import { IChunk } from '@/interfaces/database/knowledge'; | import { IChunk } from '@/interfaces/database/knowledge'; | ||||||
| import { | import { Avatar, Button, Drawer, Flex, Input, List, Spin } from 'antd'; | ||||||
|   Avatar, |  | ||||||
|   Button, |  | ||||||
|   Drawer, |  | ||||||
|   Flex, |  | ||||||
|   Input, |  | ||||||
|   List, |  | ||||||
|   Skeleton, |  | ||||||
|   Spin, |  | ||||||
| } from 'antd'; |  | ||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||||
| import { | import { | ||||||
| @ -32,20 +23,24 @@ import SvgIcon from '@/components/svg-icon'; | |||||||
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||||
| import { useGetDocumentUrl } from '@/hooks/documentHooks'; | import { useGetDocumentUrl } from '@/hooks/documentHooks'; | ||||||
| import { getExtension, isPdf } from '@/utils/documentUtils'; | import { getExtension, isPdf } from '@/utils/documentUtils'; | ||||||
|  | import { buildMessageItemReference } from '../utils'; | ||||||
| import styles from './index.less'; | import styles from './index.less'; | ||||||
| 
 | 
 | ||||||
| const MessageItem = ({ | const MessageItem = ({ | ||||||
|   item, |   item, | ||||||
|   reference, |   reference, | ||||||
|  |   loading = false, | ||||||
|   clickDocumentButton, |   clickDocumentButton, | ||||||
| }: { | }: { | ||||||
|   item: Message; |   item: Message; | ||||||
|   reference: IReference; |   reference: IReference; | ||||||
|  |   loading?: boolean; | ||||||
|   clickDocumentButton: (documentId: string, chunk: IChunk) => void; |   clickDocumentButton: (documentId: string, chunk: IChunk) => void; | ||||||
| }) => { | }) => { | ||||||
|   const userInfo = useSelectUserInfo(); |   const userInfo = useSelectUserInfo(); | ||||||
|   const fileThumbnails = useSelectFileThumbnails(); |   const fileThumbnails = useSelectFileThumbnails(); | ||||||
|   const getDocumentUrl = useGetDocumentUrl(); |   const getDocumentUrl = useGetDocumentUrl(); | ||||||
|  |   const { t } = useTranslate('chat'); | ||||||
| 
 | 
 | ||||||
|   const isAssistant = item.role === MessageType.Assistant; |   const isAssistant = item.role === MessageType.Assistant; | ||||||
| 
 | 
 | ||||||
| @ -53,6 +48,14 @@ const MessageItem = ({ | |||||||
|     return reference?.doc_aggs ?? []; |     return reference?.doc_aggs ?? []; | ||||||
|   }, [reference?.doc_aggs]); |   }, [reference?.doc_aggs]); | ||||||
| 
 | 
 | ||||||
|  |   const content = useMemo(() => { | ||||||
|  |     let text = item.content; | ||||||
|  |     if (text === '') { | ||||||
|  |       text = t('searching'); | ||||||
|  |     } | ||||||
|  |     return loading ? text?.concat('~~2$$') : text; | ||||||
|  |   }, [item.content, loading, t]); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|       className={classNames(styles.messageItem, { |       className={classNames(styles.messageItem, { | ||||||
| @ -85,15 +88,11 @@ const MessageItem = ({ | |||||||
|           <Flex vertical gap={8} flex={1}> |           <Flex vertical gap={8} flex={1}> | ||||||
|             <b>{isAssistant ? '' : userInfo.nickname}</b> |             <b>{isAssistant ? '' : userInfo.nickname}</b> | ||||||
|             <div className={styles.messageText}> |             <div className={styles.messageText}> | ||||||
|               {item.content !== '' ? ( |  | ||||||
|               <MarkdownContent |               <MarkdownContent | ||||||
|                   content={item.content} |                 content={content} | ||||||
|                 reference={reference} |                 reference={reference} | ||||||
|                 clickDocumentButton={clickDocumentButton} |                 clickDocumentButton={clickDocumentButton} | ||||||
|               ></MarkdownContent> |               ></MarkdownContent> | ||||||
|               ) : ( |  | ||||||
|                 <Skeleton active className={styles.messageEmpty} /> |  | ||||||
|               )} |  | ||||||
|             </div> |             </div> | ||||||
|             {isAssistant && referenceDocumentList.length > 0 && ( |             {isAssistant && referenceDocumentList.length > 0 && ( | ||||||
|               <List |               <List | ||||||
| @ -139,13 +138,19 @@ const ChatContainer = () => { | |||||||
|     currentConversation: conversation, |     currentConversation: conversation, | ||||||
|     addNewestConversation, |     addNewestConversation, | ||||||
|     removeLatestMessage, |     removeLatestMessage, | ||||||
|  |     addNewestAnswer, | ||||||
|   } = useFetchConversationOnMount(); |   } = useFetchConversationOnMount(); | ||||||
|   const { |   const { | ||||||
|     handleInputChange, |     handleInputChange, | ||||||
|     handlePressEnter, |     handlePressEnter, | ||||||
|     value, |     value, | ||||||
|     loading: sendLoading, |     loading: sendLoading, | ||||||
|   } = useSendMessage(conversation, addNewestConversation, removeLatestMessage); |   } = useSendMessage( | ||||||
|  |     conversation, | ||||||
|  |     addNewestConversation, | ||||||
|  |     removeLatestMessage, | ||||||
|  |     addNewestAnswer, | ||||||
|  |   ); | ||||||
|   const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = |   const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | ||||||
|     useClickDrawer(); |     useClickDrawer(); | ||||||
|   const disabled = useGetSendButtonDisabled(); |   const disabled = useGetSendButtonDisabled(); | ||||||
| @ -159,19 +164,17 @@ const ChatContainer = () => { | |||||||
|         <Flex flex={1} vertical className={styles.messageContainer}> |         <Flex flex={1} vertical className={styles.messageContainer}> | ||||||
|           <div> |           <div> | ||||||
|             <Spin spinning={loading}> |             <Spin spinning={loading}> | ||||||
|               {conversation?.message?.map((message) => { |               {conversation?.message?.map((message, i) => { | ||||||
|                 const assistantMessages = conversation?.message |  | ||||||
|                   ?.filter((x) => x.role === MessageType.Assistant) |  | ||||||
|                   .slice(1); |  | ||||||
|                 const referenceIndex = assistantMessages.findIndex( |  | ||||||
|                   (x) => x.id === message.id, |  | ||||||
|                 ); |  | ||||||
|                 const reference = conversation.reference[referenceIndex]; |  | ||||||
|                 return ( |                 return ( | ||||||
|                   <MessageItem |                   <MessageItem | ||||||
|  |                     loading={ | ||||||
|  |                       message.role === MessageType.Assistant && | ||||||
|  |                       sendLoading && | ||||||
|  |                       conversation?.message.length - 1 === i | ||||||
|  |                     } | ||||||
|                     key={message.id} |                     key={message.id} | ||||||
|                     item={message} |                     item={message} | ||||||
|                     reference={reference} |                     reference={buildMessageItemReference(conversation, message)} | ||||||
|                     clickDocumentButton={clickDocumentButton} |                     clickDocumentButton={clickDocumentButton} | ||||||
|                   ></MessageItem> |                   ></MessageItem> | ||||||
|                 ); |                 ); | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||||
| import { fileIconMap } from '@/constants/common'; | import { fileIconMap } from '@/constants/common'; | ||||||
| import { | import { | ||||||
|   useCompleteConversation, |  | ||||||
|   useCreateToken, |   useCreateToken, | ||||||
|   useFetchConversation, |   useFetchConversation, | ||||||
|   useFetchConversationList, |   useFetchConversationList, | ||||||
| @ -24,8 +23,14 @@ import { | |||||||
|   useShowDeleteConfirm, |   useShowDeleteConfirm, | ||||||
|   useTranslate, |   useTranslate, | ||||||
| } from '@/hooks/commonHooks'; | } from '@/hooks/commonHooks'; | ||||||
|  | import { useSendMessageWithSse } from '@/hooks/logicHooks'; | ||||||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | ||||||
| import { IConversation, IDialog, IStats } from '@/interfaces/database/chat'; | import { | ||||||
|  |   IAnswer, | ||||||
|  |   IConversation, | ||||||
|  |   IDialog, | ||||||
|  |   IStats, | ||||||
|  | } from '@/interfaces/database/chat'; | ||||||
| import { IChunk } from '@/interfaces/database/knowledge'; | import { IChunk } from '@/interfaces/database/knowledge'; | ||||||
| import { getFileExtension } from '@/utils'; | import { getFileExtension } from '@/utils'; | ||||||
| import { message } from 'antd'; | import { message } from 'antd'; | ||||||
| @ -380,7 +385,8 @@ export const useSelectCurrentConversation = () => { | |||||||
|   const dialog = useSelectCurrentDialog(); |   const dialog = useSelectCurrentDialog(); | ||||||
|   const { conversationId, dialogId } = useGetChatSearchParams(); |   const { conversationId, dialogId } = useGetChatSearchParams(); | ||||||
| 
 | 
 | ||||||
|   const addNewestConversation = useCallback((message: string) => { |   const addNewestConversation = useCallback( | ||||||
|  |     (message: string, answer: string = '') => { | ||||||
|       setCurrentConversation((pre) => { |       setCurrentConversation((pre) => { | ||||||
|         return { |         return { | ||||||
|           ...pre, |           ...pre, | ||||||
| @ -393,18 +399,42 @@ export const useSelectCurrentConversation = () => { | |||||||
|             } as IMessage, |             } as IMessage, | ||||||
|             { |             { | ||||||
|               role: MessageType.Assistant, |               role: MessageType.Assistant, | ||||||
|             content: '', |               content: answer, | ||||||
|               id: uuid(), |               id: uuid(), | ||||||
|               reference: [], |               reference: [], | ||||||
|             } as IMessage, |             } as IMessage, | ||||||
|           ], |           ], | ||||||
|         }; |         }; | ||||||
|       }); |       }); | ||||||
|  |     }, | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const addNewestAnswer = useCallback((answer: IAnswer) => { | ||||||
|  |     setCurrentConversation((pre) => { | ||||||
|  |       const latestMessage = pre.message?.at(-1); | ||||||
|  | 
 | ||||||
|  |       if (latestMessage) { | ||||||
|  |         return { | ||||||
|  |           ...pre, | ||||||
|  |           message: [ | ||||||
|  |             ...pre.message.slice(0, -1), | ||||||
|  |             { | ||||||
|  |               ...latestMessage, | ||||||
|  |               content: answer.answer, | ||||||
|  |               reference: answer.reference, | ||||||
|  |             } as IMessage, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return pre; | ||||||
|  |     }); | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const removeLatestMessage = useCallback(() => { |   const removeLatestMessage = useCallback(() => { | ||||||
|  |     console.info('removeLatestMessage'); | ||||||
|     setCurrentConversation((pre) => { |     setCurrentConversation((pre) => { | ||||||
|       const nextMessages = pre.message.slice(0, -2); |       const nextMessages = pre.message?.slice(0, -2) ?? []; | ||||||
|       return { |       return { | ||||||
|         ...pre, |         ...pre, | ||||||
|         message: nextMessages, |         message: nextMessages, | ||||||
| @ -441,7 +471,12 @@ export const useSelectCurrentConversation = () => { | |||||||
|     } |     } | ||||||
|   }, [conversation, conversationId]); |   }, [conversation, conversationId]); | ||||||
| 
 | 
 | ||||||
|   return { currentConversation, addNewestConversation, removeLatestMessage }; |   return { | ||||||
|  |     currentConversation, | ||||||
|  |     addNewestConversation, | ||||||
|  |     removeLatestMessage, | ||||||
|  |     addNewestAnswer, | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const useScrollToBottom = (currentConversation: IClientConversation) => { | export const useScrollToBottom = (currentConversation: IClientConversation) => { | ||||||
| @ -464,8 +499,12 @@ export const useScrollToBottom = (currentConversation: IClientConversation) => { | |||||||
| export const useFetchConversationOnMount = () => { | export const useFetchConversationOnMount = () => { | ||||||
|   const { conversationId } = useGetChatSearchParams(); |   const { conversationId } = useGetChatSearchParams(); | ||||||
|   const fetchConversation = useFetchConversation(); |   const fetchConversation = useFetchConversation(); | ||||||
|   const { currentConversation, addNewestConversation, removeLatestMessage } = |   const { | ||||||
|     useSelectCurrentConversation(); |     currentConversation, | ||||||
|  |     addNewestConversation, | ||||||
|  |     removeLatestMessage, | ||||||
|  |     addNewestAnswer, | ||||||
|  |   } = useSelectCurrentConversation(); | ||||||
|   const ref = useScrollToBottom(currentConversation); |   const ref = useScrollToBottom(currentConversation); | ||||||
| 
 | 
 | ||||||
|   const fetchConversationOnMount = useCallback(() => { |   const fetchConversationOnMount = useCallback(() => { | ||||||
| @ -483,6 +522,7 @@ export const useFetchConversationOnMount = () => { | |||||||
|     addNewestConversation, |     addNewestConversation, | ||||||
|     ref, |     ref, | ||||||
|     removeLatestMessage, |     removeLatestMessage, | ||||||
|  |     addNewestAnswer, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -504,25 +544,22 @@ export const useHandleMessageInputChange = () => { | |||||||
| 
 | 
 | ||||||
| export const useSendMessage = ( | export const useSendMessage = ( | ||||||
|   conversation: IClientConversation, |   conversation: IClientConversation, | ||||||
|   addNewestConversation: (message: string) => void, |   addNewestConversation: (message: string, answer?: string) => void, | ||||||
|   removeLatestMessage: () => void, |   removeLatestMessage: () => void, | ||||||
|  |   addNewestAnswer: (answer: IAnswer) => void, | ||||||
| ) => { | ) => { | ||||||
|   const loading = useOneNamespaceEffectsLoading('chatModel', [ |  | ||||||
|     'completeConversation', |  | ||||||
|   ]); |  | ||||||
|   const { setConversation } = useSetConversation(); |   const { setConversation } = useSetConversation(); | ||||||
|   const { conversationId } = useGetChatSearchParams(); |   const { conversationId } = useGetChatSearchParams(); | ||||||
|   const { handleInputChange, value, setValue } = useHandleMessageInputChange(); |   const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | ||||||
| 
 | 
 | ||||||
|   const fetchConversation = useFetchConversation(); |   const fetchConversation = useFetchConversation(); | ||||||
|   const completeConversation = useCompleteConversation(); |  | ||||||
| 
 | 
 | ||||||
|   const { handleClickConversation } = useClickConversationCard(); |   const { handleClickConversation } = useClickConversationCard(); | ||||||
|   // const { send } = useConnectWithSseNext();
 |   const { send, answer, done } = useSendMessageWithSse(); | ||||||
| 
 | 
 | ||||||
|   const sendMessage = useCallback( |   const sendMessage = useCallback( | ||||||
|     async (message: string, id?: string) => { |     async (message: string, id?: string) => { | ||||||
|       const retcode = await completeConversation({ |       const res: Response = await send({ | ||||||
|         conversation_id: id ?? conversationId, |         conversation_id: id ?? conversationId, | ||||||
|         messages: [ |         messages: [ | ||||||
|           ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')), |           ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')), | ||||||
| @ -533,27 +570,33 @@ export const useSendMessage = ( | |||||||
|         ], |         ], | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       if (retcode === 0) { |       if (res.status === 200) { | ||||||
|         if (id) { |         if (id) { | ||||||
|  |           console.info('111'); | ||||||
|           // new conversation
 |           // new conversation
 | ||||||
|           handleClickConversation(id); |           handleClickConversation(id); | ||||||
|         } else { |         } else { | ||||||
|           fetchConversation(conversationId); |           console.info('222'); | ||||||
|  |           // fetchConversation(conversationId);
 | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|  |         console.info('333'); | ||||||
|  | 
 | ||||||
|         // cancel loading
 |         // cancel loading
 | ||||||
|         setValue(message); |         setValue(message); | ||||||
|  |         console.info('removeLatestMessage111'); | ||||||
|         removeLatestMessage(); |         removeLatestMessage(); | ||||||
|       } |       } | ||||||
|  |       console.info('false'); | ||||||
|     }, |     }, | ||||||
|     [ |     [ | ||||||
|       conversation?.message, |       conversation?.message, | ||||||
|       conversationId, |       conversationId, | ||||||
|       fetchConversation, |       // fetchConversation,
 | ||||||
|       handleClickConversation, |       handleClickConversation, | ||||||
|       removeLatestMessage, |       removeLatestMessage, | ||||||
|       setValue, |       setValue, | ||||||
|       completeConversation, |       send, | ||||||
|     ], |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| @ -572,19 +615,27 @@ export const useSendMessage = ( | |||||||
|     [conversationId, setConversation, sendMessage], |     [conversationId, setConversation, sendMessage], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const handlePressEnter = () => { |   useEffect(() => { | ||||||
|     if (!loading) { |     if (answer.answer) { | ||||||
|  |       addNewestAnswer(answer); | ||||||
|  |       console.info('true?'); | ||||||
|  |       console.info('send msg:', answer.answer); | ||||||
|  |     } | ||||||
|  |   }, [answer, addNewestAnswer]); | ||||||
|  | 
 | ||||||
|  |   const handlePressEnter = useCallback(() => { | ||||||
|  |     if (done) { | ||||||
|       setValue(''); |       setValue(''); | ||||||
|       addNewestConversation(value); |  | ||||||
|       handleSendMessage(value.trim()); |       handleSendMessage(value.trim()); | ||||||
|     } |     } | ||||||
|   }; |     addNewestConversation(value); | ||||||
|  |   }, [addNewestConversation, handleSendMessage, done, setValue, value]); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     handlePressEnter, |     handlePressEnter, | ||||||
|     handleInputChange, |     handleInputChange, | ||||||
|     value, |     value, | ||||||
|     loading, |     loading: !done, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { IConversation, Message } from '@/interfaces/database/chat'; | import { IConversation, IReference, Message } from '@/interfaces/database/chat'; | ||||||
| import { FormInstance } from 'antd'; | import { FormInstance } from 'antd'; | ||||||
| 
 | 
 | ||||||
| export interface ISegmentedContentProps { | export interface ISegmentedContentProps { | ||||||
| @ -24,6 +24,7 @@ export type IPromptConfigParameters = Omit<VariableTableDataType, 'variable'>; | |||||||
| 
 | 
 | ||||||
| export interface IMessage extends Message { | export interface IMessage extends Message { | ||||||
|   id: string; |   id: string; | ||||||
|  |   reference?: IReference; // the latest news has reference
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IClientConversation extends IConversation { | export interface IClientConversation extends IConversation { | ||||||
|  | |||||||
| @ -23,3 +23,23 @@ | |||||||
| .referenceIcon { | .referenceIcon { | ||||||
|   padding: 0 6px; |   padding: 0 6px; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .cursor { | ||||||
|  |   display: inline-block; | ||||||
|  |   width: 1px; | ||||||
|  |   height: 16px; | ||||||
|  |   background-color: black; | ||||||
|  |   animation: blink 0.6s infinite; | ||||||
|  |   vertical-align: text-top; | ||||||
|  |   @keyframes blink { | ||||||
|  |     0% { | ||||||
|  |       opacity: 1; | ||||||
|  |     } | ||||||
|  |     50% { | ||||||
|  |       opacity: 0; | ||||||
|  |     } | ||||||
|  |     100% { | ||||||
|  |       opacity: 1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import { visitParents } from 'unist-util-visit-parents'; | |||||||
| import styles from './index.less'; | import styles from './index.less'; | ||||||
| 
 | 
 | ||||||
| const reg = /(#{2}\d+\${2})/g; | const reg = /(#{2}\d+\${2})/g; | ||||||
|  | const curReg = /(~{2}\d+\${2})/g; | ||||||
| 
 | 
 | ||||||
| const getChunkIndex = (match: string) => Number(match.slice(2, -2)); | 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.
 | // TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
 | ||||||
| @ -61,7 +62,7 @@ const MarkdownContent = ({ | |||||||
|     (chunkIndex: number) => { |     (chunkIndex: number) => { | ||||||
|       const chunks = reference?.chunks ?? []; |       const chunks = reference?.chunks ?? []; | ||||||
|       const chunkItem = chunks[chunkIndex]; |       const chunkItem = chunks[chunkIndex]; | ||||||
|       const document = reference?.doc_aggs.find( |       const document = reference?.doc_aggs?.find( | ||||||
|         (x) => x?.doc_id === chunkItem?.doc_id, |         (x) => x?.doc_id === chunkItem?.doc_id, | ||||||
|       ); |       ); | ||||||
|       const documentId = document?.doc_id; |       const documentId = document?.doc_id; | ||||||
| @ -129,7 +130,7 @@ const MarkdownContent = ({ | |||||||
| 
 | 
 | ||||||
|   const renderReference = useCallback( |   const renderReference = useCallback( | ||||||
|     (text: string) => { |     (text: string) => { | ||||||
|       return reactStringReplace(text, reg, (match, i) => { |       let replacedText = reactStringReplace(text, reg, (match, i) => { | ||||||
|         const chunkIndex = getChunkIndex(match); |         const chunkIndex = getChunkIndex(match); | ||||||
|         return ( |         return ( | ||||||
|           <Popover content={getPopoverContent(chunkIndex)}> |           <Popover content={getPopoverContent(chunkIndex)}> | ||||||
| @ -137,6 +138,12 @@ const MarkdownContent = ({ | |||||||
|           </Popover> |           </Popover> | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|  | 
 | ||||||
|  |       replacedText = reactStringReplace(replacedText, curReg, (match, i) => ( | ||||||
|  |         <span className={styles.cursor} key={i}></span> | ||||||
|  |       )); | ||||||
|  | 
 | ||||||
|  |       return replacedText; | ||||||
|     }, |     }, | ||||||
|     [getPopoverContent], |     [getPopoverContent], | ||||||
|   ); |   ); | ||||||
|  | |||||||
| @ -1,51 +1,11 @@ | |||||||
| import { useEffect } from 'react'; |  | ||||||
| import { |  | ||||||
|   useCreateSharedConversationOnMount, |  | ||||||
|   useSelectCurrentSharedConversation, |  | ||||||
|   useSendSharedMessage, |  | ||||||
| } from '../shared-hooks'; |  | ||||||
| import ChatContainer from './large'; | import ChatContainer from './large'; | ||||||
| 
 | 
 | ||||||
| import styles from './index.less'; | import styles from './index.less'; | ||||||
| 
 | 
 | ||||||
| const SharedChat = () => { | const SharedChat = () => { | ||||||
|   const { conversationId } = useCreateSharedConversationOnMount(); |  | ||||||
|   const { |  | ||||||
|     currentConversation, |  | ||||||
|     addNewestConversation, |  | ||||||
|     removeLatestMessage, |  | ||||||
|     ref, |  | ||||||
|     loading, |  | ||||||
|     setCurrentConversation, |  | ||||||
|   } = useSelectCurrentSharedConversation(conversationId); |  | ||||||
| 
 |  | ||||||
|   const { |  | ||||||
|     handlePressEnter, |  | ||||||
|     handleInputChange, |  | ||||||
|     value, |  | ||||||
|     loading: sendLoading, |  | ||||||
|   } = useSendSharedMessage( |  | ||||||
|     currentConversation, |  | ||||||
|     addNewestConversation, |  | ||||||
|     removeLatestMessage, |  | ||||||
|     setCurrentConversation, |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     console.info(location.href); |  | ||||||
|   }, []); |  | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <div className={styles.chatWrapper}> |     <div className={styles.chatWrapper}> | ||||||
|       <ChatContainer |       <ChatContainer></ChatContainer> | ||||||
|         value={value} |  | ||||||
|         handleInputChange={handleInputChange} |  | ||||||
|         handlePressEnter={handlePressEnter} |  | ||||||
|         loading={loading} |  | ||||||
|         sendLoading={sendLoading} |  | ||||||
|         conversation={currentConversation} |  | ||||||
|         ref={ref} |  | ||||||
|       ></ChatContainer> |  | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,18 +1,50 @@ | |||||||
| import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; | import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; | ||||||
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||||
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||||
| import { Message } from '@/interfaces/database/chat'; | import { IReference, Message } from '@/interfaces/database/chat'; | ||||||
| import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd'; | import { Avatar, Button, Flex, Input, List, Spin } from 'antd'; | ||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
| import { useSelectConversationLoading } from '../hooks'; |  | ||||||
| 
 | 
 | ||||||
| import HightLightMarkdown from '@/components/highlight-markdown'; | import NewDocumentLink from '@/components/new-document-link'; | ||||||
| import React, { ChangeEventHandler, forwardRef } from 'react'; | import SvgIcon from '@/components/svg-icon'; | ||||||
| import { IClientConversation } from '../interface'; | import { useGetDocumentUrl } from '@/hooks/documentHooks'; | ||||||
|  | import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; | ||||||
|  | import { getExtension, isPdf } from '@/utils/documentUtils'; | ||||||
|  | import { forwardRef, useMemo } from 'react'; | ||||||
|  | import MarkdownContent from '../markdown-content'; | ||||||
|  | import { | ||||||
|  |   useCreateSharedConversationOnMount, | ||||||
|  |   useSelectCurrentSharedConversation, | ||||||
|  |   useSendSharedMessage, | ||||||
|  | } from '../shared-hooks'; | ||||||
|  | import { buildMessageItemReference } from '../utils'; | ||||||
| import styles from './index.less'; | import styles from './index.less'; | ||||||
| 
 | 
 | ||||||
| const MessageItem = ({ item }: { item: Message }) => { | const MessageItem = ({ | ||||||
|  |   item, | ||||||
|  |   reference, | ||||||
|  |   loading = false, | ||||||
|  | }: { | ||||||
|  |   item: Message; | ||||||
|  |   reference: IReference; | ||||||
|  |   loading?: boolean; | ||||||
|  | }) => { | ||||||
|   const isAssistant = item.role === MessageType.Assistant; |   const isAssistant = item.role === MessageType.Assistant; | ||||||
|  |   const { t } = useTranslate('chat'); | ||||||
|  |   const fileThumbnails = useSelectFileThumbnails(); | ||||||
|  |   const getDocumentUrl = useGetDocumentUrl(); | ||||||
|  | 
 | ||||||
|  |   const referenceDocumentList = useMemo(() => { | ||||||
|  |     return reference?.doc_aggs ?? []; | ||||||
|  |   }, [reference?.doc_aggs]); | ||||||
|  | 
 | ||||||
|  |   const content = useMemo(() => { | ||||||
|  |     let text = item.content; | ||||||
|  |     if (text === '') { | ||||||
|  |       text = t('searching'); | ||||||
|  |     } | ||||||
|  |     return loading ? text?.concat('~~2$$') : text; | ||||||
|  |   }, [item.content, loading, t]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
| @ -45,12 +77,43 @@ const MessageItem = ({ item }: { item: Message }) => { | |||||||
|           <Flex vertical gap={8} flex={1}> |           <Flex vertical gap={8} flex={1}> | ||||||
|             <b>{isAssistant ? '' : 'You'}</b> |             <b>{isAssistant ? '' : 'You'}</b> | ||||||
|             <div className={styles.messageText}> |             <div className={styles.messageText}> | ||||||
|               {item.content !== '' ? ( |               <MarkdownContent | ||||||
|                 <HightLightMarkdown>{item.content}</HightLightMarkdown> |                 reference={reference} | ||||||
|               ) : ( |                 clickDocumentButton={() => {}} | ||||||
|                 <Skeleton active className={styles.messageEmpty} /> |                 content={content} | ||||||
|               )} |               ></MarkdownContent> | ||||||
|             </div> |             </div> | ||||||
|  |             {isAssistant && referenceDocumentList.length > 0 && ( | ||||||
|  |               <List | ||||||
|  |                 bordered | ||||||
|  |                 dataSource={referenceDocumentList} | ||||||
|  |                 renderItem={(item) => { | ||||||
|  |                   const fileThumbnail = fileThumbnails[item.doc_id]; | ||||||
|  |                   const fileExtension = getExtension(item.doc_name); | ||||||
|  |                   return ( | ||||||
|  |                     <List.Item> | ||||||
|  |                       <Flex gap={'small'} align="center"> | ||||||
|  |                         {fileThumbnail ? ( | ||||||
|  |                           <img src={fileThumbnail}></img> | ||||||
|  |                         ) : ( | ||||||
|  |                           <SvgIcon | ||||||
|  |                             name={`file-icon/${fileExtension}`} | ||||||
|  |                             width={24} | ||||||
|  |                           ></SvgIcon> | ||||||
|  |                         )} | ||||||
|  | 
 | ||||||
|  |                         <NewDocumentLink | ||||||
|  |                           link={getDocumentUrl(item.doc_id)} | ||||||
|  |                           preventDefault={!isPdf(item.doc_name)} | ||||||
|  |                         > | ||||||
|  |                           {item.doc_name} | ||||||
|  |                         </NewDocumentLink> | ||||||
|  |                       </Flex> | ||||||
|  |                     </List.Item> | ||||||
|  |                   ); | ||||||
|  |                 }} | ||||||
|  |               /> | ||||||
|  |             )} | ||||||
|           </Flex> |           </Flex> | ||||||
|         </div> |         </div> | ||||||
|       </section> |       </section> | ||||||
| @ -58,28 +121,31 @@ const MessageItem = ({ item }: { item: Message }) => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| interface IProps { | const ChatContainer = () => { | ||||||
|   handlePressEnter(): void; |   const { t } = useTranslate('chat'); | ||||||
|   handleInputChange: ChangeEventHandler<HTMLInputElement>; |   const { conversationId } = useCreateSharedConversationOnMount(); | ||||||
|   value: string; |   const { | ||||||
|   loading: boolean; |     currentConversation: conversation, | ||||||
|   sendLoading: boolean; |     addNewestConversation, | ||||||
|   conversation: IClientConversation; |     removeLatestMessage, | ||||||
|   ref: React.LegacyRef<any>; |     ref, | ||||||
| } |     loading, | ||||||
|  |     setCurrentConversation, | ||||||
|  |     addNewestAnswer, | ||||||
|  |   } = useSelectCurrentSharedConversation(conversationId); | ||||||
| 
 | 
 | ||||||
| const ChatContainer = ( |   const { | ||||||
|   { |  | ||||||
|     handlePressEnter, |     handlePressEnter, | ||||||
|     handleInputChange, |     handleInputChange, | ||||||
|     value, |     value, | ||||||
|     loading: sendLoading, |     loading: sendLoading, | ||||||
|  |   } = useSendSharedMessage( | ||||||
|     conversation, |     conversation, | ||||||
|   }: IProps, |     addNewestConversation, | ||||||
|   ref: React.LegacyRef<any>, |     removeLatestMessage, | ||||||
| ) => { |     setCurrentConversation, | ||||||
|   const loading = useSelectConversationLoading(); |     addNewestAnswer, | ||||||
|   const { t } = useTranslate('chat'); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @ -87,9 +153,18 @@ const ChatContainer = ( | |||||||
|         <Flex flex={1} vertical className={styles.messageContainer}> |         <Flex flex={1} vertical className={styles.messageContainer}> | ||||||
|           <div> |           <div> | ||||||
|             <Spin spinning={loading}> |             <Spin spinning={loading}> | ||||||
|               {conversation?.message?.map((message) => { |               {conversation?.message?.map((message, i) => { | ||||||
|                 return ( |                 return ( | ||||||
|                   <MessageItem key={message.id} item={message}></MessageItem> |                   <MessageItem | ||||||
|  |                     key={message.id} | ||||||
|  |                     item={message} | ||||||
|  |                     reference={buildMessageItemReference(conversation, message)} | ||||||
|  |                     loading={ | ||||||
|  |                       message.role === MessageType.Assistant && | ||||||
|  |                       sendLoading && | ||||||
|  |                       conversation?.message.length - 1 === i | ||||||
|  |                     } | ||||||
|  |                   ></MessageItem> | ||||||
|                 ); |                 ); | ||||||
|               })} |               })} | ||||||
|             </Spin> |             </Spin> | ||||||
|  | |||||||
| @ -1,10 +1,12 @@ | |||||||
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||||
| import { | import { | ||||||
|   useCompleteSharedConversation, |  | ||||||
|   useCreateSharedConversation, |   useCreateSharedConversation, | ||||||
|   useFetchSharedConversation, |   useFetchSharedConversation, | ||||||
| } from '@/hooks/chatHooks'; | } from '@/hooks/chatHooks'; | ||||||
|  | import { useSendMessageWithSse } from '@/hooks/logicHooks'; | ||||||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | ||||||
|  | import { IAnswer } from '@/interfaces/database/chat'; | ||||||
|  | import api from '@/utils/api'; | ||||||
| import omit from 'lodash/omit'; | import omit from 'lodash/omit'; | ||||||
| import { | import { | ||||||
|   Dispatch, |   Dispatch, | ||||||
| @ -76,6 +78,27 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => { | |||||||
|     }); |     }); | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|  |   const addNewestAnswer = useCallback((answer: IAnswer) => { | ||||||
|  |     setCurrentConversation((pre) => { | ||||||
|  |       const latestMessage = pre.message?.at(-1); | ||||||
|  | 
 | ||||||
|  |       if (latestMessage) { | ||||||
|  |         return { | ||||||
|  |           ...pre, | ||||||
|  |           message: [ | ||||||
|  |             ...pre.message.slice(0, -1), | ||||||
|  |             { | ||||||
|  |               ...latestMessage, | ||||||
|  |               content: answer.answer, | ||||||
|  |               reference: answer.reference, | ||||||
|  |             } as IMessage, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return pre; | ||||||
|  |     }); | ||||||
|  |   }, []); | ||||||
|  | 
 | ||||||
|   const removeLatestMessage = useCallback(() => { |   const removeLatestMessage = useCallback(() => { | ||||||
|     setCurrentConversation((pre) => { |     setCurrentConversation((pre) => { | ||||||
|       const nextMessages = pre.message.slice(0, -2); |       const nextMessages = pre.message.slice(0, -2); | ||||||
| @ -106,6 +129,7 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => { | |||||||
|     loading, |     loading, | ||||||
|     ref, |     ref, | ||||||
|     setCurrentConversation, |     setCurrentConversation, | ||||||
|  |     addNewestAnswer, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -114,20 +138,19 @@ export const useSendSharedMessage = ( | |||||||
|   addNewestConversation: (message: string) => void, |   addNewestConversation: (message: string) => void, | ||||||
|   removeLatestMessage: () => void, |   removeLatestMessage: () => void, | ||||||
|   setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>, |   setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>, | ||||||
|  |   addNewestAnswer: (answer: IAnswer) => void, | ||||||
| ) => { | ) => { | ||||||
|   const conversationId = conversation.id; |   const conversationId = conversation.id; | ||||||
|   const loading = useOneNamespaceEffectsLoading('chatModel', [ |  | ||||||
|     'completeExternalConversation', |  | ||||||
|   ]); |  | ||||||
|   const setConversation = useCreateSharedConversation(); |   const setConversation = useCreateSharedConversation(); | ||||||
|   const { handleInputChange, value, setValue } = useHandleMessageInputChange(); |   const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | ||||||
| 
 | 
 | ||||||
|   const fetchConversation = useFetchSharedConversation(); |   const { send, answer, done } = useSendMessageWithSse( | ||||||
|   const completeConversation = useCompleteSharedConversation(); |     api.completeExternalConversation, | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   const sendMessage = useCallback( |   const sendMessage = useCallback( | ||||||
|     async (message: string, id?: string) => { |     async (message: string, id?: string) => { | ||||||
|       const retcode = await completeConversation({ |       const res: Response = await send({ | ||||||
|         conversation_id: id ?? conversationId, |         conversation_id: id ?? conversationId, | ||||||
|         quote: false, |         quote: false, | ||||||
|         messages: [ |         messages: [ | ||||||
| @ -139,11 +162,11 @@ export const useSendSharedMessage = ( | |||||||
|         ], |         ], | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       if (retcode === 0) { |       if (res?.status === 200) { | ||||||
|         const data = await fetchConversation(conversationId); |         // const data = await fetchConversation(conversationId);
 | ||||||
|         if (data.retcode === 0) { |         // if (data.retcode === 0) {
 | ||||||
|           setCurrentConversation(data.data); |         //   setCurrentConversation(data.data);
 | ||||||
|         } |         // }
 | ||||||
|       } else { |       } else { | ||||||
|         // cancel loading
 |         // cancel loading
 | ||||||
|         setValue(message); |         setValue(message); | ||||||
| @ -153,11 +176,11 @@ export const useSendSharedMessage = ( | |||||||
|     [ |     [ | ||||||
|       conversationId, |       conversationId, | ||||||
|       conversation?.message, |       conversation?.message, | ||||||
|       fetchConversation, |       // fetchConversation,
 | ||||||
|       removeLatestMessage, |       removeLatestMessage, | ||||||
|       setValue, |       setValue, | ||||||
|       completeConversation, |       send, | ||||||
|       setCurrentConversation, |       // setCurrentConversation,
 | ||||||
|     ], |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| @ -176,18 +199,24 @@ export const useSendSharedMessage = ( | |||||||
|     [conversationId, setConversation, sendMessage], |     [conversationId, setConversation, sendMessage], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const handlePressEnter = () => { |   useEffect(() => { | ||||||
|     if (!loading) { |     if (answer.answer) { | ||||||
|  |       addNewestAnswer(answer); | ||||||
|  |     } | ||||||
|  |   }, [answer, addNewestAnswer]); | ||||||
|  | 
 | ||||||
|  |   const handlePressEnter = useCallback(() => { | ||||||
|  |     if (done) { | ||||||
|       setValue(''); |       setValue(''); | ||||||
|       addNewestConversation(value); |       addNewestConversation(value); | ||||||
|       handleSendMessage(value.trim()); |       handleSendMessage(value.trim()); | ||||||
|     } |     } | ||||||
|   }; |   }, [addNewestConversation, done, handleSendMessage, setValue, value]); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     handlePressEnter, |     handlePressEnter, | ||||||
|     handleInputChange, |     handleInputChange, | ||||||
|     value, |     value, | ||||||
|     loading, |     loading: !done, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
|  | import { MessageType } from '@/constants/chat'; | ||||||
| import { IConversation, IReference } from '@/interfaces/database/chat'; | import { IConversation, IReference } from '@/interfaces/database/chat'; | ||||||
| import { EmptyConversationId, variableEnabledFieldMap } from './constants'; | import { EmptyConversationId, variableEnabledFieldMap } from './constants'; | ||||||
|  | import { IClientConversation, IMessage } from './interface'; | ||||||
| 
 | 
 | ||||||
| export const excludeUnEnabledVariables = (values: any) => { | export const excludeUnEnabledVariables = (values: any) => { | ||||||
|   const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> = |   const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> = | ||||||
| @ -20,7 +22,7 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => { | |||||||
|   const documentIds = data.reference.reduce( |   const documentIds = data.reference.reduce( | ||||||
|     (pre: Array<string>, cur: IReference) => { |     (pre: Array<string>, cur: IReference) => { | ||||||
|       cur.doc_aggs |       cur.doc_aggs | ||||||
|         .map((x) => x.doc_id) |         ?.map((x) => x.doc_id) | ||||||
|         .forEach((x) => { |         .forEach((x) => { | ||||||
|           if (pre.every((y) => y !== x)) { |           if (pre.every((y) => y !== x)) { | ||||||
|             pre.push(x); |             pre.push(x); | ||||||
| @ -32,3 +34,20 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => { | |||||||
|   ); |   ); | ||||||
|   return documentIds.join(','); |   return documentIds.join(','); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const buildMessageItemReference = ( | ||||||
|  |   conversation: IClientConversation, | ||||||
|  |   message: IMessage, | ||||||
|  | ) => { | ||||||
|  |   const assistantMessages = conversation.message | ||||||
|  |     ?.filter((x) => x.role === MessageType.Assistant) | ||||||
|  |     .slice(1); | ||||||
|  |   const referenceIndex = assistantMessages.findIndex( | ||||||
|  |     (x) => x.id === message.id, | ||||||
|  |   ); | ||||||
|  |   const reference = message?.reference | ||||||
|  |     ? message?.reference | ||||||
|  |     : conversation.reference[referenceIndex]; | ||||||
|  | 
 | ||||||
|  |   return reference; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Authorization, Token, UserInfo } from '@/constants/authorization'; | import { Authorization, Token, UserInfo } from '@/constants/authorization'; | ||||||
| 
 | import { getSearchValue } from './commonUtil'; | ||||||
| const KeySet = [Authorization, Token, UserInfo]; | const KeySet = [Authorization, Token, UserInfo]; | ||||||
| 
 | 
 | ||||||
| const storage = { | const storage = { | ||||||
| @ -21,7 +21,7 @@ const storage = { | |||||||
|   setToken: (value: string) => { |   setToken: (value: string) => { | ||||||
|     localStorage.setItem(Token, value); |     localStorage.setItem(Token, value); | ||||||
|   }, |   }, | ||||||
|   setUserInfo: (value: string | Object) => { |   setUserInfo: (value: string | Record<string, unknown>) => { | ||||||
|     let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value; |     let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value; | ||||||
|     localStorage.setItem(UserInfo, valueStr); |     localStorage.setItem(UserInfo, valueStr); | ||||||
|   }, |   }, | ||||||
| @ -46,4 +46,13 @@ const storage = { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const getAuthorization = () => { | ||||||
|  |   const sharedId = getSearchValue('shared_id'); | ||||||
|  |   const authorization = sharedId | ||||||
|  |     ? 'Bearer ' + sharedId | ||||||
|  |     : storage.getAuthorization() || ''; | ||||||
|  | 
 | ||||||
|  |   return authorization; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export default storage; | export default storage; | ||||||
|  | |||||||
| @ -1,12 +1,12 @@ | |||||||
| import { Authorization } from '@/constants/authorization'; | import { Authorization } from '@/constants/authorization'; | ||||||
| import i18n from '@/locales/config'; | import i18n from '@/locales/config'; | ||||||
| import authorizationUtil from '@/utils/authorizationUtil'; | import authorizationUtil, { getAuthorization } from '@/utils/authorizationUtil'; | ||||||
| import { message, notification } from 'antd'; | import { message, notification } from 'antd'; | ||||||
| import { history } from 'umi'; | import { history } from 'umi'; | ||||||
| import { RequestMethod, extend } from 'umi-request'; | import { RequestMethod, extend } from 'umi-request'; | ||||||
| import { convertTheKeysOfTheObjectToSnake, getSearchValue } from './commonUtil'; | import { convertTheKeysOfTheObjectToSnake } from './commonUtil'; | ||||||
| 
 | 
 | ||||||
| const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
 | const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; | ||||||
| 
 | 
 | ||||||
| const RetcodeMessage = { | const RetcodeMessage = { | ||||||
|   200: i18n.t('message.200'), |   200: i18n.t('message.200'), | ||||||
| @ -41,9 +41,7 @@ type ResultCode = | |||||||
|   | 502 |   | 502 | ||||||
|   | 503 |   | 503 | ||||||
|   | 504; |   | 504; | ||||||
| /** | 
 | ||||||
|  * 异常处理程序 |  | ||||||
|  */ |  | ||||||
| interface ResponseType { | interface ResponseType { | ||||||
|   retcode: number; |   retcode: number; | ||||||
|   data: any; |   data: any; | ||||||
| @ -55,7 +53,6 @@ const errorHandler = (error: { | |||||||
|   message: string; |   message: string; | ||||||
| }): Response => { | }): Response => { | ||||||
|   const { response } = error; |   const { response } = error; | ||||||
|   // 手动中断请求 abort
 |  | ||||||
|   if (error.message === ABORT_REQUEST_ERR_MESSAGE) { |   if (error.message === ABORT_REQUEST_ERR_MESSAGE) { | ||||||
|     console.log('user abort  request'); |     console.log('user abort  request'); | ||||||
|   } else { |   } else { | ||||||
| @ -77,20 +74,13 @@ const errorHandler = (error: { | |||||||
|   return response; |   return response; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * 配置request请求时的默认参数 |  | ||||||
|  */ |  | ||||||
| const request: RequestMethod = extend({ | const request: RequestMethod = extend({ | ||||||
|   errorHandler, // 默认错误处理
 |   errorHandler, | ||||||
|   timeout: 300000, |   timeout: 300000, | ||||||
|   getResponse: true, |   getResponse: true, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| request.interceptors.request.use((url: string, options: any) => { | request.interceptors.request.use((url: string, options: any) => { | ||||||
|   const sharedId = getSearchValue('shared_id'); |  | ||||||
|   const authorization = sharedId |  | ||||||
|     ? 'Bearer ' + sharedId |  | ||||||
|     : authorizationUtil.getAuthorization(); |  | ||||||
|   const data = convertTheKeysOfTheObjectToSnake(options.data); |   const data = convertTheKeysOfTheObjectToSnake(options.data); | ||||||
|   const params = convertTheKeysOfTheObjectToSnake(options.params); |   const params = convertTheKeysOfTheObjectToSnake(options.params); | ||||||
| 
 | 
 | ||||||
| @ -101,7 +91,9 @@ request.interceptors.request.use((url: string, options: any) => { | |||||||
|       data, |       data, | ||||||
|       params, |       params, | ||||||
|       headers: { |       headers: { | ||||||
|         ...(options.skipToken ? undefined : { [Authorization]: authorization }), |         ...(options.skipToken | ||||||
|  |           ? undefined | ||||||
|  |           : { [Authorization]: getAuthorization() }), | ||||||
|         ...options.headers, |         ...options.headers, | ||||||
|       }, |       }, | ||||||
|       interceptors: true, |       interceptors: true, | ||||||
| @ -109,16 +101,11 @@ request.interceptors.request.use((url: string, options: any) => { | |||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| /* |  | ||||||
|  * 请求response拦截器 |  | ||||||
|  * */ |  | ||||||
| 
 |  | ||||||
| request.interceptors.response.use(async (response: any, options) => { | request.interceptors.response.use(async (response: any, options) => { | ||||||
|   if (options.responseType === 'blob') { |   if (options.responseType === 'blob') { | ||||||
|     return response; |     return response; | ||||||
|   } |   } | ||||||
|   const data: ResponseType = await response.clone().json(); |   const data: ResponseType = await response.clone().json(); | ||||||
|   // response 拦截
 |  | ||||||
| 
 | 
 | ||||||
|   if (data.retcode === 401 || data.retcode === 401) { |   if (data.retcode === 401 || data.retcode === 401) { | ||||||
|     notification.error({ |     notification.error({ | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 balibabu
						balibabu