瀏覽代碼

feat: Select derived messages from backend #2088 (#2176)

### What problem does this PR solve?

feat: Select derived messages from backend #2088

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
tags/v0.11.0
balibabu 1 年之前
父節點
當前提交
5400467da1
沒有連結到貢獻者的電子郵件帳戶。

+ 12
- 10
web/src/components/message-item/group-button.tsx 查看文件

interface UserGroupButtonProps extends Partial<IRemoveMessageById> { interface UserGroupButtonProps extends Partial<IRemoveMessageById> {
messageId: string; messageId: string;
content: string; content: string;
regenerateMessage(): void;
regenerateMessage?: () => void;
sendLoading: boolean; sendLoading: boolean;
} }


<Radio.Button value="a"> <Radio.Button value="a">
<CopyToClipboard text={content}></CopyToClipboard> <CopyToClipboard text={content}></CopyToClipboard>
</Radio.Button> </Radio.Button>
<Radio.Button
value="b"
onClick={regenerateMessage}
disabled={sendLoading}
>
<Tooltip title={t('chat.regenerate')}>
<SyncOutlined spin={sendLoading} />
</Tooltip>
</Radio.Button>
{regenerateMessage && (
<Radio.Button
value="b"
onClick={regenerateMessage}
disabled={sendLoading}
>
<Tooltip title={t('chat.regenerate')}>
<SyncOutlined spin={sendLoading} />
</Tooltip>
</Radio.Button>
)}
{removeMessageById && ( {removeMessageById && (
<Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
<Tooltip title={t('common.delete')}> <Tooltip title={t('common.delete')}>

+ 4
- 2
web/src/components/message-item/index.tsx 查看文件

); );


const handleRegenerateMessage = useCallback(() => { const handleRegenerateMessage = useCallback(() => {
regenerateMessage(item);
regenerateMessage?.(item);
}, [regenerateMessage, item]); }, [regenerateMessage, item]);


useEffect(() => { useEffect(() => {
content={item.content} content={item.content}
messageId={item.id} messageId={item.id}
removeMessageById={removeMessageById} removeMessageById={removeMessageById}
regenerateMessage={handleRegenerateMessage}
regenerateMessage={
regenerateMessage && handleRegenerateMessage
}
sendLoading={sendLoading} sendLoading={sendLoading}
></UserGroupButton> ></UserGroupButton>
)} )}

+ 8
- 21
web/src/hooks/chat-hooks.ts 查看文件

IDialog, IDialog,
IStats, IStats,
IToken, IToken,
Message,
} from '@/interfaces/database/chat'; } from '@/interfaces/database/chat';
import { IFeedbackRequestBody } from '@/interfaces/request/chat'; import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import { IClientConversation, IMessage } from '@/pages/chat/interface';
import { IClientConversation } from '@/pages/chat/interface';
import chatService from '@/services/chat-service'; import chatService from '@/services/chat-service';
import { buildMessageUuid, isConversationIdExist } from '@/utils/chat';
import { buildMessageListWithUuid, isConversationIdExist } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd'; import { message } from 'antd';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';


const buildMessageListWithUuid = (messages?: Message[]) => {
return (
messages?.map((x: Message | IMessage) => ({
...x,
id: buildMessageUuid(x),
})) ?? []
);
};

//#region logic //#region logic


export const useClickDialogCard = () => { export const useClickDialogCard = () => {
return { data, loading, createSharedConversation: mutateAsync }; return { data, loading, createSharedConversation: mutateAsync };
}; };


export const useFetchNextSharedConversation = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchSharedConversation'],
mutationFn: async (conversationId: string) => {
export const useFetchNextSharedConversation = (conversationId: string) => {
const { data, isPending: loading } = useQuery({
queryKey: ['fetchSharedConversation'],
enabled: !!conversationId,
queryFn: async () => {
const { data } = await chatService.getExternalConversation( const { data } = await chatService.getExternalConversation(
null, null,
conversationId, conversationId,
}, },
}); });


return { data, loading, fetchConversation: mutateAsync };
return { data, loading };
}; };


//#endregion //#endregion

+ 8
- 0
web/src/hooks/flow-hooks.ts 查看文件

import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import flowService from '@/services/flow-service'; import flowService from '@/services/flow-service';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd'; import { message } from 'antd';
import { set } from 'lodash';
import get from 'lodash/get';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';


queryFn: async () => { queryFn: async () => {
const { data } = await flowService.getCanvas({}, id); const { data } = await flowService.getCanvas({}, id);


const messageList = buildMessageListWithUuid(
get(data, 'data.dsl.messages', []),
);
set(data, 'data.dsl.messages', messageList);

return data?.data ?? {}; return data?.data ?? {};
}, },
}); });

+ 112
- 4
web/src/hooks/logic-hooks.ts 查看文件

import { Authorization } from '@/constants/authorization'; import { Authorization } from '@/constants/authorization';
import { MessageType } from '@/constants/chat';
import { LanguageTranslationMap } from '@/constants/common'; import { LanguageTranslationMap } from '@/constants/common';
import { Pagination } from '@/interfaces/common'; import { Pagination } from '@/interfaces/common';
import { ResponseType } from '@/interfaces/database/base'; import { ResponseType } from '@/interfaces/database/base';
import { IAnswer, Message } from '@/interfaces/database/chat'; import { IAnswer, Message } 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 { IClientConversation } from '@/pages/chat/interface';
import { IClientConversation, IMessage } from '@/pages/chat/interface';
import api from '@/utils/api'; import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util'; import { getAuthorization } from '@/utils/authorization-util';
import { getMessagePureId } from '@/utils/chat';
import { buildMessageUuid, getMessagePureId } from '@/utils/chat';
import { PaginationProps } from 'antd'; import { PaginationProps } from 'antd';
import { FormInstance } from 'antd/lib'; import { FormInstance } from 'antd/lib';
import axios from 'axios'; import axios from 'axios';
}; };
}; };


export const useSelectDerivedMessages = () => {
const [derivedMessages, setDerivedMessages] = useState<IMessage[]>([]);

const ref = useScrollToBottom(derivedMessages);

const addNewestQuestion = useCallback(
(message: Message, answer: string = '') => {
setDerivedMessages((pre) => {
return [
...pre,
{
...message,
id: buildMessageUuid(message),
},
{
role: MessageType.Assistant,
content: answer,
id: buildMessageUuid({ ...message, role: MessageType.Assistant }),
},
];
});
},
[],
);

// Add the streaming message to the last item in the message list
const addNewestAnswer = useCallback((answer: IAnswer) => {
setDerivedMessages((pre) => {
return [
...(pre?.slice(0, -1) ?? []),
{
role: MessageType.Assistant,
content: answer.answer,
reference: answer.reference,
id: buildMessageUuid({
id: answer.id,
role: MessageType.Assistant,
}),
prompt: answer.prompt,
},
];
});
}, []);

const removeLatestMessage = useCallback(() => {
setDerivedMessages((pre) => {
const nextMessages = pre?.slice(0, -2) ?? [];
return nextMessages;
});
}, []);

const removeMessageById = useCallback(
(messageId: string) => {
setDerivedMessages((pre) => {
const nextMessages =
pre?.filter(
(x) => getMessagePureId(x.id) !== getMessagePureId(messageId),
) ?? [];
return nextMessages;
});
},
[setDerivedMessages],
);

const removeMessagesAfterCurrentMessage = useCallback(
(messageId: string) => {
setDerivedMessages((pre) => {
const index = pre.findIndex((x) => x.id === messageId);
if (index !== -1) {
let nextMessages = pre.slice(0, index + 2) ?? [];
const latestMessage = nextMessages.at(-1);
nextMessages = latestMessage
? [
...nextMessages.slice(0, -1),
{
...latestMessage,
content: '',
reference: undefined,
prompt: undefined,
},
]
: nextMessages;
return nextMessages;
}
return pre;
});
},
[setDerivedMessages],
);

return {
ref,
derivedMessages,
setDerivedMessages,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
};
};

export interface IRemoveMessageById { export interface IRemoveMessageById {
removeMessageById(messageId: string): void; removeMessageById(messageId: string): void;
} }
}; };


export interface IRegenerateMessage { export interface IRegenerateMessage {
regenerateMessage(message: Message): void;
regenerateMessage?: (message: Message) => void;
} }


export const useRegenerateMessage = ({ export const useRegenerateMessage = ({
messages, messages,
}: { }: {
removeMessagesAfterCurrentMessage(messageId: string): void; removeMessagesAfterCurrentMessage(messageId: string): void;
sendMessage({ message }: { message: Message; messages?: Message[] }): void;
sendMessage({
message,
}: {
message: Message;
messages?: Message[];
}): void | Promise<any>;
messages: Message[]; messages: Message[];
}) => { }) => {
const regenerateMessage = useCallback( const regenerateMessage = useCallback(

+ 22
- 22
web/src/pages/chat/chat-container/index.tsx 查看文件

import { import {
useClickDrawer, useClickDrawer,
useCreateConversationBeforeUploadDocument, useCreateConversationBeforeUploadDocument,
useFetchConversationOnMount,
useGetFileIcon, useGetFileIcon,
useGetSendButtonDisabled, useGetSendButtonDisabled,
useSendButtonDisabled, useSendButtonDisabled,
useSendMessage,
useSendNextMessage,
} from '../hooks'; } from '../hooks';
import { buildMessageItemReference } from '../utils'; import { buildMessageItemReference } from '../utils';


import MessageInput from '@/components/message-input'; import MessageInput from '@/components/message-input';
import {
useFetchNextConversation,
useGetChatSearchParams,
} from '@/hooks/chat-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { memo } from 'react'; import { memo } from 'react';
import styles from './index.less'; import styles from './index.less';


const ChatContainer = () => { const ChatContainer = () => {
const { conversationId } = useGetChatSearchParams();
const { data: conversation } = useFetchNextConversation();

const { const {
ref, ref,
currentConversation: conversation,
addNewestConversation,
removeLatestMessage,
addNewestAnswer,
conversationId,
loading, loading,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useFetchConversationOnMount();
const {
sendLoading,
derivedMessages,
handleInputChange, handleInputChange,
handlePressEnter, handlePressEnter,
value, value,
loading: sendLoading,
regenerateMessage, regenerateMessage,
} = useSendMessage(
conversation,
addNewestConversation,
removeLatestMessage,
addNewestAnswer,
removeMessagesAfterCurrentMessage,
);
removeMessageById,
} = useSendNextMessage();

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, i) => {
{derivedMessages?.map((message, i) => {
return ( return (
<MessageItem <MessageItem
loading={ loading={
message.role === MessageType.Assistant && message.role === MessageType.Assistant &&
sendLoading && sendLoading &&
conversation?.message.length - 1 === i
derivedMessages.length - 1 === i
} }
key={message.id} key={message.id}
item={message} item={message}
nickname={userInfo.nickname} nickname={userInfo.nickname}
avatar={userInfo.avatar} avatar={userInfo.avatar}
reference={buildMessageItemReference(conversation, message)}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: conversation.reference,
},
message,
)}
clickDocumentButton={clickDocumentButton} clickDocumentButton={clickDocumentButton}
index={i} index={i}
removeMessageById={removeMessageById} removeMessageById={removeMessageById}

+ 193
- 10
web/src/pages/chat/hooks.ts 查看文件

useRegenerateMessage, useRegenerateMessage,
useRemoveMessageById, useRemoveMessageById,
useRemoveMessagesAfterCurrentMessage, useRemoveMessagesAfterCurrentMessage,
useScrollToBottom,
useSelectDerivedMessages,
useSendMessageWithSse, useSendMessageWithSse,
} from '@/hooks/logic-hooks'; } from '@/hooks/logic-hooks';
import { import {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from 'react'; } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
}; };
}; };


export const useScrollToBottom = (currentConversation: IClientConversation) => {
const ref = useRef<HTMLDivElement>(null);
// export const useScrollToBottom = (currentConversation: IClientConversation) => {
// const ref = useRef<HTMLDivElement>(null);


const scrollToBottom = useCallback(() => {
if (currentConversation.id) {
ref.current?.scrollIntoView({ behavior: 'instant' });
// const scrollToBottom = useCallback(() => {
// if (currentConversation.id) {
// ref.current?.scrollIntoView({ behavior: 'instant' });
// }
// }, [currentConversation]);

// useEffect(() => {
// scrollToBottom();
// }, [scrollToBottom]);

// return ref;
// };

export const useSelectNextMessages = () => {
const {
ref,
setDerivedMessages,
derivedMessages,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectDerivedMessages();
const { data: conversation, loading } = useFetchNextConversation();
const { data: dialog } = useFetchNextDialog();
const { conversationId, dialogId } = useGetChatSearchParams();

const addPrologue = useCallback(() => {
if (dialogId !== '' && conversationId === '') {
const prologue = dialog.prompt_config?.prologue;

const nextMessage = {
role: MessageType.Assistant,
content: prologue,
id: uuid(),
} as IMessage;

setDerivedMessages([nextMessage]);
} }
}, [currentConversation]);
}, [conversationId, dialog, dialogId, setDerivedMessages]);

useEffect(() => {
addPrologue();
}, [addPrologue]);


useEffect(() => { useEffect(() => {
scrollToBottom();
}, [scrollToBottom]);
if (conversationId) {
setDerivedMessages(conversation.message);
}
}, [conversation.message, conversationId, setDerivedMessages]);


return ref;
return {
ref,
derivedMessages,
loading,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
};
}; };


export const useFetchConversationOnMount = () => { export const useFetchConversationOnMount = () => {
}; };
}; };


export const useSendNextMessage = () => {
const { setConversation } = useSetConversation();
const { conversationId } = useGetChatSearchParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { handleClickConversation } = useClickConversationCard();
const { send, answer, done, setDone } = useSendMessageWithSse();
const {
ref,
derivedMessages,
loading,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectNextMessages();

const sendMessage = useCallback(
async ({
message,
currentConversationId,
messages,
}: {
message: Message;
currentConversationId?: string;
messages?: Message[];
}) => {
const res = await send({
conversation_id: currentConversationId ?? conversationId,
messages: [...(messages ?? derivedMessages ?? []), message],
});

if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) {
// cancel loading
setValue(message.content);
console.info('removeLatestMessage111');
removeLatestMessage();
} else {
if (currentConversationId) {
console.info('111');
// new conversation
handleClickConversation(currentConversationId);
} else {
console.info('222');
// fetchConversation(conversationId);
}
}
},
[
derivedMessages,
conversationId,
handleClickConversation,
removeLatestMessage,
setValue,
send,
],
);

const handleSendMessage = useCallback(
async (message: Message) => {
if (conversationId !== '') {
sendMessage({ message });
} else {
const data = await setConversation(message.content);
if (data.retcode === 0) {
const id = data.data.id;
sendMessage({ message, currentConversationId: id });
}
}
},
[conversationId, setConversation, sendMessage],
);

const { regenerateMessage } = useRegenerateMessage({
removeMessagesAfterCurrentMessage,
sendMessage,
messages: derivedMessages,
});

useEffect(() => {
// #1289
if (answer.answer && answer?.conversationId === conversationId) {
addNewestAnswer(answer);
}
}, [answer, addNewestAnswer, conversationId]);

useEffect(() => {
// #1289 switch to another conversion window when the last conversion answer doesn't finish.
if (conversationId) {
setDone(true);
}
}, [setDone, conversationId]);

const handlePressEnter = useCallback(
(documentIds: string[]) => {
if (trim(value) === '') return;
const id = uuid();

addNewestQuestion({
content: value,
doc_ids: documentIds,
id,
role: MessageType.User,
});
if (done) {
setValue('');
handleSendMessage({
id,
content: value.trim(),
role: MessageType.User,
doc_ids: documentIds,
});
}
},
[addNewestQuestion, handleSendMessage, done, setValue, value],
);

return {
handlePressEnter,
handleInputChange,
value,
setValue,
regenerateMessage,
sendLoading: !done,
loading,
ref,
derivedMessages,
removeMessageById,
};
};

export const useGetFileIcon = () => { export const useGetFileIcon = () => {
const getFileIcon = (filename: string) => { const getFileIcon = (filename: string) => {
const ext: string = getFileExtension(filename); const ext: string = getFileExtension(filename);

+ 16
- 21
web/src/pages/chat/share/large.tsx 查看文件

import MessageInput from '@/components/message-input'; import MessageInput from '@/components/message-input';
import MessageItem from '@/components/message-item'; import MessageItem from '@/components/message-item';
import { MessageType, SharedFrom } from '@/constants/chat'; import { MessageType, SharedFrom } from '@/constants/chat';
import { useFetchNextSharedConversation } from '@/hooks/chat-hooks';
import { useSendButtonDisabled } from '@/pages/chat/hooks'; import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { Flex, Spin } from 'antd'; import { Flex, Spin } from 'antd';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { import {
useCreateSharedConversationOnMount, useCreateSharedConversationOnMount,
useGetSharedChatSearchParams, useGetSharedChatSearchParams,
useSelectCurrentSharedConversation,
useSendSharedMessage, useSendSharedMessage,
} from '../shared-hooks'; } from '../shared-hooks';
import { buildMessageItemReference } from '../utils'; import { buildMessageItemReference } from '../utils';


const ChatContainer = () => { const ChatContainer = () => {
const { conversationId } = useCreateSharedConversationOnMount(); const { conversationId } = useCreateSharedConversationOnMount();
const {
currentConversation: conversation,
addNewestConversation,
removeLatestMessage,
ref,
loading,
setCurrentConversation,
addNewestAnswer,
} = useSelectCurrentSharedConversation(conversationId);
const { data } = useFetchNextSharedConversation(conversationId);


const { const {
handlePressEnter, handlePressEnter,
handleInputChange, handleInputChange,
value, value,
loading: sendLoading,
} = useSendSharedMessage(
conversation,
addNewestConversation,
removeLatestMessage,
setCurrentConversation,
addNewestAnswer,
);
sendLoading,
loading,
ref,
derivedMessages,
} = useSendSharedMessage(conversationId);
const sendDisabled = useSendButtonDisabled(value); const sendDisabled = useSendButtonDisabled(value);
const { from } = useGetSharedChatSearchParams(); const { from } = useGetSharedChatSearchParams();


<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, i) => {
{derivedMessages?.map((message, i) => {
return ( return (
<MessageItem <MessageItem
key={message.id} key={message.id}
item={message} item={message}
nickname="You" nickname="You"
reference={buildMessageItemReference(conversation, message)}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: data?.data?.reference,
},
message,
)}
loading={ loading={
message.role === MessageType.Assistant && message.role === MessageType.Assistant &&
sendLoading && sendLoading &&
conversation?.message.length - 1 === i
derivedMessages?.length - 1 === i
} }
index={i} index={i}
></MessageItem> ></MessageItem>

+ 41
- 112
web/src/pages/chat/shared-hooks.ts 查看文件

useCreateNextSharedConversation, useCreateNextSharedConversation,
useFetchNextSharedConversation, useFetchNextSharedConversation,
} from '@/hooks/chat-hooks'; } from '@/hooks/chat-hooks';
import { useSendMessageWithSse } from '@/hooks/logic-hooks';
import { IAnswer, Message } from '@/interfaces/database/chat';
import {
useSelectDerivedMessages,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import { Message } from '@/interfaces/database/chat';
import api from '@/utils/api'; import api from '@/utils/api';
import { buildMessageUuid } from '@/utils/chat';
import trim from 'lodash/trim'; import trim from 'lodash/trim';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { useHandleMessageInputChange, useScrollToBottom } from './hooks';
import { IClientConversation, IMessage } from './interface';
import { useHandleMessageInputChange } from './hooks';


export const useCreateSharedConversationOnMount = () => { export const useCreateSharedConversationOnMount = () => {
const [currentQueryParameters] = useSearchParams(); const [currentQueryParameters] = useSearchParams();
return { conversationId }; return { conversationId };
}; };


export const useSelectCurrentSharedConversation = (conversationId: string) => {
const [currentConversation, setCurrentConversation] =
useState<IClientConversation>({} as IClientConversation);
const { fetchConversation, loading } = useFetchNextSharedConversation();

const ref = useScrollToBottom(currentConversation);

const addNewestConversation = useCallback((message: Partial<Message>) => {
setCurrentConversation((pre) => {
return {
...pre,
message: [
...(pre.message ?? []),
{
...message,
id: buildMessageUuid(message),
} as IMessage,
{
role: MessageType.Assistant,
content: '',
id: buildMessageUuid({ ...message, role: MessageType.Assistant }),
reference: {},
} 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,
id: buildMessageUuid({
id: answer.id,
role: MessageType.Assistant,
}),
prompt: answer.prompt,
} as IMessage,
],
};
}
return pre;
});
}, []);

const removeLatestMessage = useCallback(() => {
setCurrentConversation((pre) => {
const nextMessages = pre.message.slice(0, -2);
return {
...pre,
message: nextMessages,
};
});
}, []);

const fetchConversationOnMount = useCallback(async () => {
if (conversationId) {
const data = await fetchConversation(conversationId);
if (data.retcode === 0) {
setCurrentConversation(data.data);
}
}
}, [conversationId, fetchConversation]);
export const useSelectNextSharedMessages = (conversationId: string) => {
const { data, loading } = useFetchNextSharedConversation(conversationId);

const {
derivedMessages,
ref,
setDerivedMessages,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
} = useSelectDerivedMessages();


useEffect(() => { useEffect(() => {
fetchConversationOnMount();
}, [fetchConversationOnMount]);
setDerivedMessages(data?.data?.message);
}, [setDerivedMessages, data]);


return { return {
currentConversation,
addNewestConversation,
derivedMessages,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage, removeLatestMessage,
loading, loading,
ref, ref,
setCurrentConversation,
addNewestAnswer,
setDerivedMessages,
}; };
}; };


return trim(value) === ''; return trim(value) === '';
}; };


export const useSendSharedMessage = (
conversation: IClientConversation,
addNewestConversation: (message: Partial<Message>, answer?: string) => void,
removeLatestMessage: () => void,
setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>,
addNewestAnswer: (answer: IAnswer) => void,
) => {
const conversationId = conversation.id;
export const useSendSharedMessage = (conversationId: string) => {
const { createSharedConversation: setConversation } = const { createSharedConversation: setConversation } =
useCreateNextSharedConversation(); useCreateNextSharedConversation();
const { handleInputChange, value, setValue } = useHandleMessageInputChange(); const { handleInputChange, value, setValue } = useHandleMessageInputChange();

const { send, answer, done } = useSendMessageWithSse( const { send, answer, done } = useSendMessageWithSse(
api.completeExternalConversation, api.completeExternalConversation,
); );
const {
derivedMessages,
ref,
removeLatestMessage,
addNewestAnswer,
addNewestQuestion,
loading,
} = useSelectNextSharedMessages(conversationId);


const sendMessage = useCallback( const sendMessage = useCallback(
async (message: Message, id?: string) => { async (message: Message, id?: string) => {
const res = await send({ const res = await send({
conversation_id: id ?? conversationId, conversation_id: id ?? conversationId,
quote: false, quote: false,
messages: [...(conversation?.message ?? []), message],
messages: [...(derivedMessages ?? []), message],
}); });


if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) {
removeLatestMessage(); removeLatestMessage();
} }
}, },
[
conversationId,
conversation?.message,
// fetchConversation,
removeLatestMessage,
setValue,
send,
// setCurrentConversation,
],
[conversationId, derivedMessages, removeLatestMessage, setValue, send],
); );


const handleSendMessage = useCallback( const handleSendMessage = useCallback(
const id = uuid(); const id = uuid();
if (done) { if (done) {
setValue(''); setValue('');
addNewestConversation({
addNewestQuestion({
content: value, content: value,
doc_ids: documentIds, doc_ids: documentIds,
id, id,
}); });
} }
}, },
[addNewestConversation, done, handleSendMessage, setValue, value],
[addNewestQuestion, done, handleSendMessage, setValue, value],
); );


return { return {
handlePressEnter, handlePressEnter,
handleInputChange, handleInputChange,
value, value,
loading: !done,
sendLoading: !done,
ref,
loading,
derivedMessages,
}; };
}; };



+ 1
- 1
web/src/pages/chat/utils.ts 查看文件

); );
const reference = message?.reference const reference = message?.reference
? message?.reference ? message?.reference
: conversation.reference[referenceIndex];
: (conversation?.reference ?? {})[referenceIndex];


return reference; return reference;
}; };

+ 12
- 17
web/src/pages/flow/chat/box.tsx 查看文件

import { buildMessageItemReference } from '@/pages/chat/utils'; import { buildMessageItemReference } from '@/pages/chat/utils';
import { Button, Drawer, Flex, Input, Spin } from 'antd'; import { Button, Drawer, Flex, Input, Spin } from 'antd';


import { useSelectCurrentMessages, useSendMessage } from './hooks';
import { useSendNextMessage } from './hooks';


import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import styles from './index.less'; import styles from './index.less';


const FlowChatBox = () => { const FlowChatBox = () => {
const { const {
ref,
currentMessages,
reference,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
loading,
} = useSelectCurrentMessages();

const {
sendLoading,
handleInputChange, handleInputChange,
handlePressEnter, handlePressEnter,
value, value,
loading: sendLoading,
} = useSendMessage(addNewestQuestion, removeLatestMessage, addNewestAnswer);
loading,
ref,
derivedMessages,
reference,
} = useSendNextMessage();

const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
useGetFileIcon(); useGetFileIcon();
<Flex flex={1} vertical className={styles.messageContainer}> <Flex flex={1} vertical className={styles.messageContainer}>
<div> <div>
<Spin spinning={loading}> <Spin spinning={loading}>
{currentMessages?.map((message, i) => {
{derivedMessages?.map((message, i) => {
return ( return (
<MessageItem <MessageItem
loading={ loading={
message.role === MessageType.Assistant && message.role === MessageType.Assistant &&
sendLoading && sendLoading &&
currentMessages.length - 1 === i
derivedMessages.length - 1 === i
} }
key={message.id} key={message.id}
nickname={userInfo.nickname} nickname={userInfo.nickname}
avatar={userInfo.avatar} avatar={userInfo.avatar}
item={message} item={message}
reference={buildMessageItemReference( reference={buildMessageItemReference(
{ message: currentMessages, reference },
{ message: derivedMessages, reference },
message, message,
)} )}
clickDocumentButton={clickDocumentButton} clickDocumentButton={clickDocumentButton}
index={i} index={i}
regenerateMessage={() => {}}
showLikeButton={false} showLikeButton={false}
sendLoading={sendLoading}
></MessageItem> ></MessageItem>
); );
})} })}

+ 118
- 0
web/src/pages/flow/chat/hooks.ts 查看文件

import { import {
useHandleMessageInputChange, useHandleMessageInputChange,
useScrollToBottom, useScrollToBottom,
useSelectDerivedMessages,
useSendMessageWithSse, useSendMessageWithSse,
} from '@/hooks/logic-hooks'; } from '@/hooks/logic-hooks';
import { IAnswer, Message } from '@/interfaces/database/chat'; import { IAnswer, Message } from '@/interfaces/database/chat';
}; };
}; };


export const useSelectNextMessages = () => {
const { id: id } = useParams();
const { data: flowDetail, loading } = useFetchFlow();
const messages = flowDetail.dsl.messages;
const reference = flowDetail.dsl.reference;
const {
derivedMessages,
setDerivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectDerivedMessages();

useEffect(() => {
if (id) {
const nextMessages = messages.map((x) => ({ ...x, id: uuid() }));
setDerivedMessages(nextMessages);
}
}, [messages, id, setDerivedMessages]);

return {
reference,
loading,
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
};
};

export const useSendMessage = ( export const useSendMessage = (
addNewestQuestion: (message: Message, answer?: string) => void, addNewestQuestion: (message: Message, answer?: string) => void,
removeLatestMessage: () => void, removeLatestMessage: () => void,
loading: !done, loading: !done,
}; };
}; };

export const useSendNextMessage = () => {
const {
reference,
loading,
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
} = useSelectNextMessages();
const { id: flowId } = useParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { refetch } = useFetchFlow();

const { send, answer, done } = useSendMessageWithSse(api.runCanvas);

const sendMessage = useCallback(
async ({ message }: { message: Message; messages?: Message[] }) => {
const params: Record<string, unknown> = {
id: flowId,
};
if (message.content) {
params.message = message.content;
params.message_id = message.id;
}
const res = await send(params);

if (receiveMessageError(res)) {
antMessage.error(res?.data?.retmsg);

// cancel loading
setValue(message.content);
removeLatestMessage();
} else {
refetch(); // pull the message list after sending the message successfully
}
},
[flowId, removeLatestMessage, setValue, send, refetch],
);

const handleSendMessage = useCallback(
async (message: Message) => {
sendMessage({ message });
},
[sendMessage],
);

useEffect(() => {
if (answer.answer) {
addNewestAnswer(answer);
}
}, [answer, addNewestAnswer]);

const handlePressEnter = useCallback(() => {
if (trim(value) === '') return;
const id = uuid();
if (done) {
setValue('');
handleSendMessage({ id, content: value.trim(), role: MessageType.User });
}
addNewestQuestion({
content: value,
id,
role: MessageType.User,
});
}, [addNewestQuestion, handleSendMessage, done, setValue, value]);

return {
handlePressEnter,
handleInputChange,
value,
sendLoading: !done,
reference,
loading,
derivedMessages,
ref,
removeMessageById,
};
};

+ 9
- 0
web/src/utils/chat.ts 查看文件

} }
return id; return id;
}; };

export const buildMessageListWithUuid = (messages?: Message[]) => {
return (
messages?.map((x: Message | IMessage) => ({
...x,
id: buildMessageUuid(x),
})) ?? []
);
};

Loading…
取消
儲存