### What problem does this PR solve? feat: Support for conversational streaming #709 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.6.0
| PORT=9222 | 
| "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", | ||||
| "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", | 
| "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", | ||||
| "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", | 
| 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> | 
| return removeConversation; | return removeConversation; | ||||
| }; | }; | ||||
| /* | |||||
| @deprecated | |||||
| */ | |||||
| export const useCompleteConversation = () => { | export const useCompleteConversation = () => { | ||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| 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 | 
| 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 { getSearchValue } from '@/utils/commonUtil'; | |||||
| import { getAuthorization } from '@/utils/authorizationUtil'; | |||||
| 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'; | ||||
| return appConf; | return appConf; | ||||
| }; | }; | ||||
| export const useConnectWithSse = (url: string) => { | |||||
| const [content, setContent] = useState<string>(''); | |||||
| 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 useSendMessageWithSse = ( | |||||
| url: string = api.completeConversation, | |||||
| ) => { | |||||
| const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); | |||||
| const [done, setDone] = useState(true); | |||||
| 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, { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| [Authorization]: authorization, | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| body: JSON.stringify(body), | |||||
| }); | |||||
| const reader = response?.body | |||||
| ?.pipeThrough(new TextDecoderStream()) | |||||
| .getReader(); | |||||
| // const reader = response.body.getReader(); | |||||
| while (true) { | |||||
| const { value, done } = await reader?.read(); | |||||
| console.log('Received', value); | |||||
| setContent(value); | |||||
| if (done) break; | |||||
| try { | |||||
| setDone(false); | |||||
| const response = await fetch(url, { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| [Authorization]: getAuthorization(), | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| body: JSON.stringify(body), | |||||
| }); | |||||
| const reader = response?.body | |||||
| ?.pipeThrough(new TextDecoderStream()) | |||||
| .pipeThrough(new EventSourceParserStream()) | |||||
| .getReader(); | |||||
| while (true) { | |||||
| const x = await reader?.read(); | |||||
| if (x) { | |||||
| const { done, value } = x; | |||||
| 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; | |||||
| } catch (e) { | |||||
| setDone(true); | |||||
| console.warn(e); | |||||
| } | } | ||||
| return response; | |||||
| }, | }, | ||||
| [authorization], | |||||
| [url], | |||||
| ); | ); | ||||
| return { send, content }; | |||||
| return { send, answer, done }; | |||||
| }; | }; | 
| 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; | 
| comingSoon: 'Coming Soon', | comingSoon: 'Coming Soon', | ||||
| download: 'Download', | download: 'Download', | ||||
| close: 'Close', | close: 'Close', | ||||
| preview: 'Preview', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: 'Sign in', | login: 'Sign in', | ||||
| 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', | 
| comingSoon: '即將推出', | comingSoon: '即將推出', | ||||
| download: '下載', | download: '下載', | ||||
| close: '关闭', | close: '关闭', | ||||
| preview: '預覽', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登入', | login: '登入', | ||||
| partialTitle: '部分嵌入', | partialTitle: '部分嵌入', | ||||
| extensionTitle: 'Chrome 插件', | extensionTitle: 'Chrome 插件', | ||||
| tokenError: '請先創建 Api Token!', | tokenError: '請先創建 Api Token!', | ||||
| searching: '搜索中', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概述', | profile: '概述', | 
| comingSoon: '即将推出', | comingSoon: '即将推出', | ||||
| download: '下载', | download: '下载', | ||||
| close: '关闭', | close: '关闭', | ||||
| preview: '预览', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登录', | login: '登录', | ||||
| partialTitle: '部分嵌入', | partialTitle: '部分嵌入', | ||||
| extensionTitle: 'Chrome 插件', | extensionTitle: 'Chrome 插件', | ||||
| tokenError: '请先创建 Api Token!', | tokenError: '请先创建 Api Token!', | ||||
| searching: '搜索中', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概要', | profile: '概要', | 
| 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 { | |||||
| Avatar, | |||||
| Button, | |||||
| Drawer, | |||||
| Flex, | |||||
| Input, | |||||
| List, | |||||
| Skeleton, | |||||
| Spin, | |||||
| } from 'antd'; | |||||
| import { Avatar, Button, Drawer, Flex, Input, List, Spin } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { | import { | ||||
| 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; | ||||
| 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, { | ||||
| <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 | |||||
| content={item.content} | |||||
| reference={reference} | |||||
| clickDocumentButton={clickDocumentButton} | |||||
| ></MarkdownContent> | |||||
| ) : ( | |||||
| <Skeleton active className={styles.messageEmpty} /> | |||||
| )} | |||||
| <MarkdownContent | |||||
| content={content} | |||||
| reference={reference} | |||||
| clickDocumentButton={clickDocumentButton} | |||||
| ></MarkdownContent> | |||||
| </div> | </div> | ||||
| {isAssistant && referenceDocumentList.length > 0 && ( | {isAssistant && referenceDocumentList.length > 0 && ( | ||||
| <List | <List | ||||
| 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(); | ||||
| <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) => { | |||||
| 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]; | |||||
| {conversation?.message?.map((message, i) => { | |||||
| 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> | ||||
| ); | ); | 
| 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, | ||||
| 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'; | ||||
| 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) => { | |||||
| return { | |||||
| ...pre, | |||||
| message: [ | |||||
| ...pre.message, | |||||
| { | |||||
| role: MessageType.User, | |||||
| content: message, | |||||
| id: uuid(), | |||||
| } as IMessage, | |||||
| { | |||||
| role: MessageType.Assistant, | |||||
| content: answer, | |||||
| id: uuid(), | |||||
| reference: [], | |||||
| } as IMessage, | |||||
| ], | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const addNewestAnswer = useCallback((answer: IAnswer) => { | |||||
| setCurrentConversation((pre) => { | setCurrentConversation((pre) => { | ||||
| return { | |||||
| ...pre, | |||||
| message: [ | |||||
| ...pre.message, | |||||
| { | |||||
| role: MessageType.User, | |||||
| content: message, | |||||
| id: uuid(), | |||||
| } as IMessage, | |||||
| { | |||||
| role: MessageType.Assistant, | |||||
| content: '', | |||||
| id: uuid(), | |||||
| reference: [], | |||||
| } as IMessage, | |||||
| ], | |||||
| }; | |||||
| 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, | ||||
| } | } | ||||
| }, [conversation, conversationId]); | }, [conversation, conversationId]); | ||||
| return { currentConversation, addNewestConversation, removeLatestMessage }; | |||||
| return { | |||||
| currentConversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| addNewestAnswer, | |||||
| }; | |||||
| }; | }; | ||||
| export const useScrollToBottom = (currentConversation: IClientConversation) => { | 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 } = | |||||
| useSelectCurrentConversation(); | |||||
| const { | |||||
| currentConversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| addNewestAnswer, | |||||
| } = useSelectCurrentConversation(); | |||||
| const ref = useScrollToBottom(currentConversation); | const ref = useScrollToBottom(currentConversation); | ||||
| const fetchConversationOnMount = useCallback(() => { | const fetchConversationOnMount = useCallback(() => { | ||||
| addNewestConversation, | addNewestConversation, | ||||
| ref, | ref, | ||||
| removeLatestMessage, | removeLatestMessage, | ||||
| addNewestAnswer, | |||||
| }; | }; | ||||
| }; | }; | ||||
| 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')), | ||||
| ], | ], | ||||
| }); | }); | ||||
| 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, | |||||
| ], | ], | ||||
| ); | ); | ||||
| [conversationId, setConversation, sendMessage], | [conversationId, setConversation, sendMessage], | ||||
| ); | ); | ||||
| const handlePressEnter = () => { | |||||
| if (!loading) { | |||||
| useEffect(() => { | |||||
| 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, | |||||
| }; | }; | ||||
| }; | }; | ||||
| 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 { | ||||
| 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 { | 
| .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; | |||||
| } | |||||
| } | |||||
| } | 
| 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. | ||||
| (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; | ||||
| 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)}> | ||||
| </Popover> | </Popover> | ||||
| ); | ); | ||||
| }); | }); | ||||
| replacedText = reactStringReplace(replacedText, curReg, (match, i) => ( | |||||
| <span className={styles.cursor} key={i}></span> | |||||
| )); | |||||
| return replacedText; | |||||
| }, | }, | ||||
| [getPopoverContent], | [getPopoverContent], | ||||
| ); | ); | 
| 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 | |||||
| value={value} | |||||
| handleInputChange={handleInputChange} | |||||
| handlePressEnter={handlePressEnter} | |||||
| loading={loading} | |||||
| sendLoading={sendLoading} | |||||
| conversation={currentConversation} | |||||
| ref={ref} | |||||
| ></ChatContainer> | |||||
| <ChatContainer></ChatContainer> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| }; | }; | 
| 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 { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd'; | |||||
| import { IReference, Message } from '@/interfaces/database/chat'; | |||||
| 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 React, { ChangeEventHandler, forwardRef } from 'react'; | |||||
| import { IClientConversation } from '../interface'; | |||||
| import NewDocumentLink from '@/components/new-document-link'; | |||||
| import SvgIcon from '@/components/svg-icon'; | |||||
| 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 | ||||
| <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 !== '' ? ( | |||||
| <HightLightMarkdown>{item.content}</HightLightMarkdown> | |||||
| ) : ( | |||||
| <Skeleton active className={styles.messageEmpty} /> | |||||
| )} | |||||
| <MarkdownContent | |||||
| reference={reference} | |||||
| clickDocumentButton={() => {}} | |||||
| 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> | ||||
| ); | ); | ||||
| }; | }; | ||||
| interface IProps { | |||||
| handlePressEnter(): void; | |||||
| handleInputChange: ChangeEventHandler<HTMLInputElement>; | |||||
| value: string; | |||||
| loading: boolean; | |||||
| sendLoading: boolean; | |||||
| conversation: IClientConversation; | |||||
| ref: React.LegacyRef<any>; | |||||
| } | |||||
| const ChatContainer = () => { | |||||
| const { t } = useTranslate('chat'); | |||||
| const { conversationId } = useCreateSharedConversationOnMount(); | |||||
| const { | |||||
| currentConversation: conversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| ref, | |||||
| loading, | |||||
| setCurrentConversation, | |||||
| addNewestAnswer, | |||||
| } = useSelectCurrentSharedConversation(conversationId); | |||||
| const ChatContainer = ( | |||||
| { | |||||
| const { | |||||
| handlePressEnter, | handlePressEnter, | ||||
| handleInputChange, | handleInputChange, | ||||
| value, | value, | ||||
| loading: sendLoading, | loading: sendLoading, | ||||
| } = useSendSharedMessage( | |||||
| conversation, | conversation, | ||||
| }: IProps, | |||||
| ref: React.LegacyRef<any>, | |||||
| ) => { | |||||
| const loading = useSelectConversationLoading(); | |||||
| const { t } = useTranslate('chat'); | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| setCurrentConversation, | |||||
| addNewestAnswer, | |||||
| ); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <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> | 
| 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, | ||||
| }); | }); | ||||
| }, []); | }, []); | ||||
| 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); | ||||
| loading, | loading, | ||||
| ref, | ref, | ||||
| setCurrentConversation, | setCurrentConversation, | ||||
| addNewestAnswer, | |||||
| }; | }; | ||||
| }; | }; | ||||
| 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 completeConversation = useCompleteSharedConversation(); | |||||
| const { send, answer, done } = useSendMessageWithSse( | |||||
| 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: [ | ||||
| ], | ], | ||||
| }); | }); | ||||
| if (retcode === 0) { | |||||
| const data = await fetchConversation(conversationId); | |||||
| if (data.retcode === 0) { | |||||
| setCurrentConversation(data.data); | |||||
| } | |||||
| if (res?.status === 200) { | |||||
| // const data = await fetchConversation(conversationId); | |||||
| // if (data.retcode === 0) { | |||||
| // setCurrentConversation(data.data); | |||||
| // } | |||||
| } else { | } else { | ||||
| // cancel loading | // cancel loading | ||||
| setValue(message); | setValue(message); | ||||
| [ | [ | ||||
| conversationId, | conversationId, | ||||
| conversation?.message, | conversation?.message, | ||||
| fetchConversation, | |||||
| // fetchConversation, | |||||
| removeLatestMessage, | removeLatestMessage, | ||||
| setValue, | setValue, | ||||
| completeConversation, | |||||
| setCurrentConversation, | |||||
| send, | |||||
| // setCurrentConversation, | |||||
| ], | ], | ||||
| ); | ); | ||||
| [conversationId, setConversation, sendMessage], | [conversationId, setConversation, sendMessage], | ||||
| ); | ); | ||||
| const handlePressEnter = () => { | |||||
| if (!loading) { | |||||
| useEffect(() => { | |||||
| 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, | |||||
| }; | }; | ||||
| }; | }; | 
| 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> = | ||||
| 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); | ||||
| ); | ); | ||||
| 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; | |||||
| }; | 
| 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 = { | ||||
| 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); | ||||
| }, | }, | ||||
| }, | }, | ||||
| }; | }; | ||||
| export const getAuthorization = () => { | |||||
| const sharedId = getSearchValue('shared_id'); | |||||
| const authorization = sharedId | |||||
| ? 'Bearer ' + sharedId | |||||
| : storage.getAuthorization() || ''; | |||||
| return authorization; | |||||
| }; | |||||
| export default storage; | export default storage; | 
| 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'), | ||||
| | 502 | | 502 | ||||
| | 503 | | 503 | ||||
| | 504; | | 504; | ||||
| /** | |||||
| * 异常处理程序 | |||||
| */ | |||||
| interface ResponseType { | interface ResponseType { | ||||
| retcode: number; | retcode: number; | ||||
| data: any; | data: any; | ||||
| 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 { | ||||
| 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); | ||||
| data, | data, | ||||
| params, | params, | ||||
| headers: { | headers: { | ||||
| ...(options.skipToken ? undefined : { [Authorization]: authorization }), | |||||
| ...(options.skipToken | |||||
| ? undefined | |||||
| : { [Authorization]: getAuthorization() }), | |||||
| ...options.headers, | ...options.headers, | ||||
| }, | }, | ||||
| interceptors: true, | interceptors: true, | ||||
| }; | }; | ||||
| }); | }); | ||||
| /* | |||||
| * 请求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({ |