diff --git a/web/.env b/web/.env index e69de29bb2..26f65746c7 100644 --- a/web/.env +++ b/web/.env @@ -0,0 +1 @@ +PORT=9222 \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index e71a6bedb8..5068f4bf17 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.6.3", "classnames": "^2.5.1", "dayjs": "^1.11.10", + "eventsource-parser": "^1.1.2", "i18next": "^23.7.16", "js-base64": "^3.7.5", "jsencrypt": "^3.3.2", @@ -10206,6 +10207,14 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", diff --git a/web/package.json b/web/package.json index 60c56d55c4..2b5b333cc2 100644 --- a/web/package.json +++ b/web/package.json @@ -3,7 +3,7 @@ "author": "zhaofengchao <13723060510@163.com>", "scripts": { "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", "lint": "umi lint --eslint-only", "setup": "umi setup", @@ -19,6 +19,7 @@ "axios": "^1.6.3", "classnames": "^2.5.1", "dayjs": "^1.11.10", + "eventsource-parser": "^1.1.2", "i18next": "^23.7.16", "js-base64": "^3.7.5", "jsencrypt": "^3.3.2", diff --git a/web/src/components/new-document-link.tsx b/web/src/components/new-document-link.tsx index 0e7620de3e..3cbc917ce6 100644 --- a/web/src/components/new-document-link.tsx +++ b/web/src/components/new-document-link.tsx @@ -18,7 +18,7 @@ const NewDocumentLink = ({ onClick={!preventDefault ? undefined : (e) => e.preventDefault()} href={link} rel="noreferrer" - style={{ color }} + style={{ color, wordBreak: 'break-all' }} > {children} diff --git a/web/src/hooks/chatHooks.ts b/web/src/hooks/chatHooks.ts index 34e0d74b0d..467accc42a 100644 --- a/web/src/hooks/chatHooks.ts +++ b/web/src/hooks/chatHooks.ts @@ -154,6 +154,9 @@ export const useRemoveConversation = () => { return removeConversation; }; +/* +@deprecated + */ export const useCompleteConversation = () => { const dispatch = useDispatch(); @@ -283,20 +286,4 @@ export const useFetchSharedConversation = () => { return fetchSharedConversation; }; -export const useCompleteSharedConversation = () => { - const dispatch = useDispatch(); - - const completeSharedConversation = useCallback( - (payload: any) => { - return dispatch({ - type: 'chatModel/completeExternalConversation', - payload: payload, - }); - }, - [dispatch], - ); - - return completeSharedConversation; -}; - //#endregion diff --git a/web/src/hooks/logicHooks.ts b/web/src/hooks/logicHooks.ts index 4b69948a5a..0b888839e7 100644 --- a/web/src/hooks/logicHooks.ts +++ b/web/src/hooks/logicHooks.ts @@ -1,13 +1,14 @@ import { Authorization } from '@/constants/authorization'; import { LanguageTranslationMap } from '@/constants/common'; import { Pagination } from '@/interfaces/common'; +import { IAnswer } from '@/interfaces/database/chat'; import { IKnowledgeFile } from '@/interfaces/database/knowledge'; import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; 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 axios from 'axios'; +import { EventSourceParserStream } from 'eventsource-parser/stream'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'umi'; @@ -138,62 +139,60 @@ export const useFetchAppConf = () => { return appConf; }; -export const useConnectWithSse = (url: string) => { - const [content, setContent] = useState(''); - - 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({} as IAnswer); + const [done, setDone] = useState(true); -export const useConnectWithSseNext = () => { - const [content, setContent] = useState(''); - const sharedId = getSearchValue('shared_id'); - const authorization = sharedId - ? 'Bearer ' + sharedId - : authorizationUtil.getAuthorization(); const send = useCallback( 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 }; }; diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index 025ce2ad14..5e87a92def 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -72,6 +72,11 @@ export interface IReference { total: number; } +export interface IAnswer { + answer: string; + reference: IReference; +} + export interface Docagg { count: number; doc_id: string; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 1a0628d562..c3089bca56 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -25,6 +25,7 @@ export default { comingSoon: 'Coming Soon', download: 'Download', close: 'Close', + preview: 'Preview', }, login: { login: 'Sign in', @@ -381,6 +382,7 @@ export default { partialTitle: 'Partial Embed', extensionTitle: 'Chrome Extension', tokenError: 'Please create API Token first!', + searching: 'searching...', }, setting: { profile: 'Profile', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 55e911f9d1..7b06ffaf3b 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -25,6 +25,7 @@ export default { comingSoon: '即將推出', download: '下載', close: '关闭', + preview: '預覽', }, login: { login: '登入', @@ -352,6 +353,7 @@ export default { partialTitle: '部分嵌入', extensionTitle: 'Chrome 插件', tokenError: '請先創建 Api Token!', + searching: '搜索中', }, setting: { profile: '概述', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index db52bf98c4..bdf035b7fb 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -25,6 +25,7 @@ export default { comingSoon: '即将推出', download: '下载', close: '关闭', + preview: '预览', }, login: { login: '登录', @@ -369,6 +370,7 @@ export default { partialTitle: '部分嵌入', extensionTitle: 'Chrome 插件', tokenError: '请先创建 Api Token!', + searching: '搜索中', }, setting: { profile: '概要', diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx index f2e465b8c8..9f062453ce 100644 --- a/web/src/pages/chat/chat-container/index.tsx +++ b/web/src/pages/chat/chat-container/index.tsx @@ -6,16 +6,7 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; import { useSelectUserInfo } from '@/hooks/userSettingHook'; import { IReference, Message } from '@/interfaces/database/chat'; 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 { useMemo } from 'react'; import { @@ -32,20 +23,24 @@ import SvgIcon from '@/components/svg-icon'; import { useTranslate } from '@/hooks/commonHooks'; import { useGetDocumentUrl } from '@/hooks/documentHooks'; import { getExtension, isPdf } from '@/utils/documentUtils'; +import { buildMessageItemReference } from '../utils'; import styles from './index.less'; const MessageItem = ({ item, reference, + loading = false, clickDocumentButton, }: { item: Message; reference: IReference; + loading?: boolean; clickDocumentButton: (documentId: string, chunk: IChunk) => void; }) => { const userInfo = useSelectUserInfo(); const fileThumbnails = useSelectFileThumbnails(); const getDocumentUrl = useGetDocumentUrl(); + const { t } = useTranslate('chat'); const isAssistant = item.role === MessageType.Assistant; @@ -53,6 +48,14 @@ const MessageItem = ({ 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 (
{isAssistant ? '' : userInfo.nickname}
- {item.content !== '' ? ( - - ) : ( - - )} +
{isAssistant && referenceDocumentList.length > 0 && ( { currentConversation: conversation, addNewestConversation, removeLatestMessage, + addNewestAnswer, } = useFetchConversationOnMount(); const { handleInputChange, handlePressEnter, value, loading: sendLoading, - } = useSendMessage(conversation, addNewestConversation, removeLatestMessage); + } = useSendMessage( + conversation, + addNewestConversation, + removeLatestMessage, + addNewestAnswer, + ); const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = useClickDrawer(); const disabled = useGetSendButtonDisabled(); @@ -159,19 +164,17 @@ const ChatContainer = () => {
- {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 ( ); diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index c13f6c7c84..7aef84389b 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -1,7 +1,6 @@ import { MessageType } from '@/constants/chat'; import { fileIconMap } from '@/constants/common'; import { - useCompleteConversation, useCreateToken, useFetchConversation, useFetchConversationList, @@ -24,8 +23,14 @@ import { useShowDeleteConfirm, useTranslate, } from '@/hooks/commonHooks'; +import { useSendMessageWithSse } from '@/hooks/logicHooks'; 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 { getFileExtension } from '@/utils'; import { message } from 'antd'; @@ -380,31 +385,56 @@ export const useSelectCurrentConversation = () => { const dialog = useSelectCurrentDialog(); 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) => { - 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(() => { + console.info('removeLatestMessage'); setCurrentConversation((pre) => { - const nextMessages = pre.message.slice(0, -2); + const nextMessages = pre.message?.slice(0, -2) ?? []; return { ...pre, message: nextMessages, @@ -441,7 +471,12 @@ export const useSelectCurrentConversation = () => { } }, [conversation, conversationId]); - return { currentConversation, addNewestConversation, removeLatestMessage }; + return { + currentConversation, + addNewestConversation, + removeLatestMessage, + addNewestAnswer, + }; }; export const useScrollToBottom = (currentConversation: IClientConversation) => { @@ -464,8 +499,12 @@ export const useScrollToBottom = (currentConversation: IClientConversation) => { export const useFetchConversationOnMount = () => { const { conversationId } = useGetChatSearchParams(); const fetchConversation = useFetchConversation(); - const { currentConversation, addNewestConversation, removeLatestMessage } = - useSelectCurrentConversation(); + const { + currentConversation, + addNewestConversation, + removeLatestMessage, + addNewestAnswer, + } = useSelectCurrentConversation(); const ref = useScrollToBottom(currentConversation); const fetchConversationOnMount = useCallback(() => { @@ -483,6 +522,7 @@ export const useFetchConversationOnMount = () => { addNewestConversation, ref, removeLatestMessage, + addNewestAnswer, }; }; @@ -504,25 +544,22 @@ export const useHandleMessageInputChange = () => { export const useSendMessage = ( conversation: IClientConversation, - addNewestConversation: (message: string) => void, + addNewestConversation: (message: string, answer?: string) => void, removeLatestMessage: () => void, + addNewestAnswer: (answer: IAnswer) => void, ) => { - const loading = useOneNamespaceEffectsLoading('chatModel', [ - 'completeConversation', - ]); const { setConversation } = useSetConversation(); const { conversationId } = useGetChatSearchParams(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); const fetchConversation = useFetchConversation(); - const completeConversation = useCompleteConversation(); const { handleClickConversation } = useClickConversationCard(); - // const { send } = useConnectWithSseNext(); + const { send, answer, done } = useSendMessageWithSse(); const sendMessage = useCallback( async (message: string, id?: string) => { - const retcode = await completeConversation({ + const res: Response = await send({ conversation_id: id ?? conversationId, messages: [ ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')), @@ -533,27 +570,33 @@ export const useSendMessage = ( ], }); - if (retcode === 0) { + if (res.status === 200) { if (id) { + console.info('111'); // new conversation handleClickConversation(id); } else { - fetchConversation(conversationId); + console.info('222'); + // fetchConversation(conversationId); } } else { + console.info('333'); + // cancel loading setValue(message); + console.info('removeLatestMessage111'); removeLatestMessage(); } + console.info('false'); }, [ conversation?.message, conversationId, - fetchConversation, + // fetchConversation, handleClickConversation, removeLatestMessage, setValue, - completeConversation, + send, ], ); @@ -572,19 +615,27 @@ export const useSendMessage = ( [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(''); - addNewestConversation(value); handleSendMessage(value.trim()); } - }; + addNewestConversation(value); + }, [addNewestConversation, handleSendMessage, done, setValue, value]); return { handlePressEnter, handleInputChange, value, - loading, + loading: !done, }; }; diff --git a/web/src/pages/chat/interface.ts b/web/src/pages/chat/interface.ts index c45da819ed..0b51921a89 100644 --- a/web/src/pages/chat/interface.ts +++ b/web/src/pages/chat/interface.ts @@ -1,4 +1,4 @@ -import { IConversation, Message } from '@/interfaces/database/chat'; +import { IConversation, IReference, Message } from '@/interfaces/database/chat'; import { FormInstance } from 'antd'; export interface ISegmentedContentProps { @@ -24,6 +24,7 @@ export type IPromptConfigParameters = Omit; export interface IMessage extends Message { id: string; + reference?: IReference; // the latest news has reference } export interface IClientConversation extends IConversation { diff --git a/web/src/pages/chat/markdown-content/index.less b/web/src/pages/chat/markdown-content/index.less index ee1301bffd..caa208017f 100644 --- a/web/src/pages/chat/markdown-content/index.less +++ b/web/src/pages/chat/markdown-content/index.less @@ -23,3 +23,23 @@ .referenceIcon { 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; + } + } +} diff --git a/web/src/pages/chat/markdown-content/index.tsx b/web/src/pages/chat/markdown-content/index.tsx index c32166f195..278fa9f471 100644 --- a/web/src/pages/chat/markdown-content/index.tsx +++ b/web/src/pages/chat/markdown-content/index.tsx @@ -16,6 +16,7 @@ import { visitParents } from 'unist-util-visit-parents'; import styles from './index.less'; const reg = /(#{2}\d+\${2})/g; +const curReg = /(~{2}\d+\${2})/g; 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. @@ -61,7 +62,7 @@ const MarkdownContent = ({ (chunkIndex: number) => { const chunks = reference?.chunks ?? []; const chunkItem = chunks[chunkIndex]; - const document = reference?.doc_aggs.find( + const document = reference?.doc_aggs?.find( (x) => x?.doc_id === chunkItem?.doc_id, ); const documentId = document?.doc_id; @@ -129,7 +130,7 @@ const MarkdownContent = ({ const renderReference = useCallback( (text: string) => { - return reactStringReplace(text, reg, (match, i) => { + let replacedText = reactStringReplace(text, reg, (match, i) => { const chunkIndex = getChunkIndex(match); return ( @@ -137,6 +138,12 @@ const MarkdownContent = ({ ); }); + + replacedText = reactStringReplace(replacedText, curReg, (match, i) => ( + + )); + + return replacedText; }, [getPopoverContent], ); diff --git a/web/src/pages/chat/share/index.tsx b/web/src/pages/chat/share/index.tsx index 00d91cdfec..acaadcbf94 100644 --- a/web/src/pages/chat/share/index.tsx +++ b/web/src/pages/chat/share/index.tsx @@ -1,51 +1,11 @@ -import { useEffect } from 'react'; -import { - useCreateSharedConversationOnMount, - useSelectCurrentSharedConversation, - useSendSharedMessage, -} from '../shared-hooks'; import ChatContainer from './large'; import styles from './index.less'; 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 (
- +
); }; diff --git a/web/src/pages/chat/share/large.tsx b/web/src/pages/chat/share/large.tsx index 1a5ba45255..4e17dda31c 100644 --- a/web/src/pages/chat/share/large.tsx +++ b/web/src/pages/chat/share/large.tsx @@ -1,18 +1,50 @@ import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; import { MessageType } from '@/constants/chat'; 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 { 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'; -const MessageItem = ({ item }: { item: Message }) => { +const MessageItem = ({ + item, + reference, + loading = false, +}: { + item: Message; + reference: IReference; + loading?: boolean; +}) => { 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 (
{ {isAssistant ? '' : 'You'}
- {item.content !== '' ? ( - {item.content} - ) : ( - - )} + {}} + content={content} + >
+ {isAssistant && referenceDocumentList.length > 0 && ( + { + const fileThumbnail = fileThumbnails[item.doc_id]; + const fileExtension = getExtension(item.doc_name); + return ( + + + {fileThumbnail ? ( + + ) : ( + + )} + + + {item.doc_name} + + + + ); + }} + /> + )}
@@ -58,28 +121,31 @@ const MessageItem = ({ item }: { item: Message }) => { ); }; -interface IProps { - handlePressEnter(): void; - handleInputChange: ChangeEventHandler; - value: string; - loading: boolean; - sendLoading: boolean; - conversation: IClientConversation; - ref: React.LegacyRef; -} +const ChatContainer = () => { + const { t } = useTranslate('chat'); + const { conversationId } = useCreateSharedConversationOnMount(); + const { + currentConversation: conversation, + addNewestConversation, + removeLatestMessage, + ref, + loading, + setCurrentConversation, + addNewestAnswer, + } = useSelectCurrentSharedConversation(conversationId); -const ChatContainer = ( - { + const { handlePressEnter, handleInputChange, value, loading: sendLoading, + } = useSendSharedMessage( conversation, - }: IProps, - ref: React.LegacyRef, -) => { - const loading = useSelectConversationLoading(); - const { t } = useTranslate('chat'); + addNewestConversation, + removeLatestMessage, + setCurrentConversation, + addNewestAnswer, + ); return ( <> @@ -87,9 +153,18 @@ const ChatContainer = (
- {conversation?.message?.map((message) => { + {conversation?.message?.map((message, i) => { return ( - + ); })} diff --git a/web/src/pages/chat/shared-hooks.ts b/web/src/pages/chat/shared-hooks.ts index 4d6877dcfa..92072a3e93 100644 --- a/web/src/pages/chat/shared-hooks.ts +++ b/web/src/pages/chat/shared-hooks.ts @@ -1,10 +1,12 @@ import { MessageType } from '@/constants/chat'; import { - useCompleteSharedConversation, useCreateSharedConversation, useFetchSharedConversation, } from '@/hooks/chatHooks'; +import { useSendMessageWithSse } from '@/hooks/logicHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import { IAnswer } from '@/interfaces/database/chat'; +import api from '@/utils/api'; import omit from 'lodash/omit'; import { 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(() => { setCurrentConversation((pre) => { const nextMessages = pre.message.slice(0, -2); @@ -106,6 +129,7 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => { loading, ref, setCurrentConversation, + addNewestAnswer, }; }; @@ -114,20 +138,19 @@ export const useSendSharedMessage = ( addNewestConversation: (message: string) => void, removeLatestMessage: () => void, setCurrentConversation: Dispatch>, + addNewestAnswer: (answer: IAnswer) => void, ) => { const conversationId = conversation.id; - const loading = useOneNamespaceEffectsLoading('chatModel', [ - 'completeExternalConversation', - ]); const setConversation = useCreateSharedConversation(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); - const fetchConversation = useFetchSharedConversation(); - const completeConversation = useCompleteSharedConversation(); + const { send, answer, done } = useSendMessageWithSse( + api.completeExternalConversation, + ); const sendMessage = useCallback( async (message: string, id?: string) => { - const retcode = await completeConversation({ + const res: Response = await send({ conversation_id: id ?? conversationId, quote: false, messages: [ @@ -139,11 +162,11 @@ export const useSendSharedMessage = ( ], }); - 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 { // cancel loading setValue(message); @@ -153,11 +176,11 @@ export const useSendSharedMessage = ( [ conversationId, conversation?.message, - fetchConversation, + // fetchConversation, removeLatestMessage, setValue, - completeConversation, - setCurrentConversation, + send, + // setCurrentConversation, ], ); @@ -176,18 +199,24 @@ export const useSendSharedMessage = ( [conversationId, setConversation, sendMessage], ); - const handlePressEnter = () => { - if (!loading) { + useEffect(() => { + if (answer.answer) { + addNewestAnswer(answer); + } + }, [answer, addNewestAnswer]); + + const handlePressEnter = useCallback(() => { + if (done) { setValue(''); addNewestConversation(value); handleSendMessage(value.trim()); } - }; + }, [addNewestConversation, done, handleSendMessage, setValue, value]); return { handlePressEnter, handleInputChange, value, - loading, + loading: !done, }; }; diff --git a/web/src/pages/chat/utils.ts b/web/src/pages/chat/utils.ts index 7c3fe80c73..7518397c9f 100644 --- a/web/src/pages/chat/utils.ts +++ b/web/src/pages/chat/utils.ts @@ -1,5 +1,7 @@ +import { MessageType } from '@/constants/chat'; import { IConversation, IReference } from '@/interfaces/database/chat'; import { EmptyConversationId, variableEnabledFieldMap } from './constants'; +import { IClientConversation, IMessage } from './interface'; export const excludeUnEnabledVariables = (values: any) => { const unEnabledFields: Array = @@ -20,7 +22,7 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => { const documentIds = data.reference.reduce( (pre: Array, cur: IReference) => { cur.doc_aggs - .map((x) => x.doc_id) + ?.map((x) => x.doc_id) .forEach((x) => { if (pre.every((y) => y !== x)) { pre.push(x); @@ -32,3 +34,20 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => { ); 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; +}; diff --git a/web/src/utils/authorizationUtil.ts b/web/src/utils/authorizationUtil.ts index f4b8b5e7e1..c0d5bde254 100644 --- a/web/src/utils/authorizationUtil.ts +++ b/web/src/utils/authorizationUtil.ts @@ -1,5 +1,5 @@ import { Authorization, Token, UserInfo } from '@/constants/authorization'; - +import { getSearchValue } from './commonUtil'; const KeySet = [Authorization, Token, UserInfo]; const storage = { @@ -21,7 +21,7 @@ const storage = { setToken: (value: string) => { localStorage.setItem(Token, value); }, - setUserInfo: (value: string | Object) => { + setUserInfo: (value: string | Record) => { let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value; 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; diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 267831db6b..2653b65809 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -1,12 +1,12 @@ import { Authorization } from '@/constants/authorization'; import i18n from '@/locales/config'; -import authorizationUtil from '@/utils/authorizationUtil'; +import authorizationUtil, { getAuthorization } from '@/utils/authorizationUtil'; import { message, notification } from 'antd'; import { history } from 'umi'; 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 = { 200: i18n.t('message.200'), @@ -41,9 +41,7 @@ type ResultCode = | 502 | 503 | 504; -/** - * 异常处理程序 - */ + interface ResponseType { retcode: number; data: any; @@ -55,7 +53,6 @@ const errorHandler = (error: { message: string; }): Response => { const { response } = error; - // 手动中断请求 abort if (error.message === ABORT_REQUEST_ERR_MESSAGE) { console.log('user abort request'); } else { @@ -77,20 +74,13 @@ const errorHandler = (error: { return response; }; -/** - * 配置request请求时的默认参数 - */ const request: RequestMethod = extend({ - errorHandler, // 默认错误处理 + errorHandler, timeout: 300000, getResponse: true, }); 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 params = convertTheKeysOfTheObjectToSnake(options.params); @@ -101,7 +91,9 @@ request.interceptors.request.use((url: string, options: any) => { data, params, headers: { - ...(options.skipToken ? undefined : { [Authorization]: authorization }), + ...(options.skipToken + ? undefined + : { [Authorization]: getAuthorization() }), ...options.headers, }, interceptors: true, @@ -109,16 +101,11 @@ request.interceptors.request.use((url: string, options: any) => { }; }); -/* - * 请求response拦截器 - * */ - request.interceptors.response.use(async (response: any, options) => { if (options.responseType === 'blob') { return response; } const data: ResponseType = await response.clone().json(); - // response 拦截 if (data.retcode === 401 || data.retcode === 401) { notification.error({