add chat plugin and split query to parse and execute (#25)

* [feature](webapp) add drill down dimensions and metric period compare and modify layout

* [feature](webapp) add drill down dimensions and metric period compare and modify layout

* [feature](webapp) gitignore add supersonic-webapp

* [feature](webapp) gitignore add supersonic-webapp

* [feature](webapp) add chat plugin and split query to parse and execute

* [feature](webapp) add chat plugin and split query to parse and execute

* [feature](webapp) add chat plugin and split query to parse and execute

---------

Co-authored-by: williamhliu <williamhliu@tencent.com>
This commit is contained in:
williamhliu
2023-08-05 22:17:42 +08:00
committed by GitHub
parent c9baed6c4e
commit 6951eada9d
86 changed files with 3193 additions and 1595 deletions

View File

@@ -1,28 +1,54 @@
import { updateMessageContainerScroll, isMobile, uuid, getLeafList } from '@/utils/utils';
import { useEffect, useRef, useState } from 'react';
import { Helmet } from 'umi';
import { Helmet, useDispatch, useLocation } from 'umi';
import MessageContainer from './MessageContainer';
import styles from './style.less';
import { ConversationDetailType, DomainType, MessageItem, MessageTypeEnum } from './type';
import { getDomainList, updateConversationName } from './service';
import {
ConversationDetailType,
DefaultEntityType,
DomainType,
MessageItem,
MessageTypeEnum,
} from './type';
import { getDomainList } from './service';
import { useThrottleFn } from 'ahooks';
import RightSection from './RightSection';
import Conversation from './Conversation';
import ChatFooter from './ChatFooter';
import classNames from 'classnames';
import { CHAT_TITLE, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from './constants';
import { cloneDeep } from 'lodash';
import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-sdk';
import { cloneDeep } from 'lodash';
import 'supersonic-chat-sdk/dist/index.css';
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
import { TOKEN_KEY } from '@/services/request';
import Conversation from './Conversation';
import { AUTH_TOKEN_KEY } from '@/common/constants';
type Props = {
isCopilotMode?: boolean;
copilotFullscreen?: boolean;
defaultDomainName?: string;
defaultEntityFilter?: DefaultEntityType;
copilotSendMsg?: string;
triggerNewConversation?: boolean;
onNewConversationTriggered?: () => void;
onCurrentDomainChange?: (domain?: DomainType) => void;
onCancelCopilotFilter?: () => void;
onCheckMoreDetail?: () => void;
};
const Chat: React.FC<Props> = ({ isCopilotMode }) => {
const isMobileMode = (isMobile || isCopilotMode) as boolean;
const Chat: React.FC<Props> = ({
isCopilotMode,
copilotFullscreen,
defaultDomainName,
defaultEntityFilter,
copilotSendMsg,
triggerNewConversation,
onNewConversationTriggered,
onCurrentDomainChange,
onCancelCopilotFilter,
onCheckMoreDetail,
}) => {
const isMobileMode = isMobile || isCopilotMode;
const localConversationCollapsed = localStorage.getItem('CONVERSATION_COLLAPSED');
const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [inputMsg, setInputMsg] = useState('');
@@ -32,68 +58,137 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
const [currentConversation, setCurrentConversation] = useState<
ConversationDetailType | undefined
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
const [currentEntity, setCurrentEntity] = useState<MsgDataType>();
const [miniProgramLoading, setMiniProgramLoading] = useState(false);
const [conversationCollapsed, setConversationCollapsed] = useState(
!localConversationCollapsed ? true : localConversationCollapsed === 'true',
);
const [domains, setDomains] = useState<DomainType[]>([]);
const [currentDomain, setCurrentDomain] = useState<DomainType>();
const [conversationCollapsed, setConversationCollapsed] = useState(false);
const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
const [applyAuthVisible, setApplyAuthVisible] = useState(false);
const [applyAuthDomain, setApplyAuthDomain] = useState('');
const [initialDomainName, setInitialDomainName] = useState('');
const location = useLocation();
const dispatch = useDispatch();
const { domainName } = (location as any).query;
const conversationRef = useRef<any>();
const chatFooterRef = useRef<any>();
useEffect(() => {
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
initDomains();
}, []);
useEffect(() => {
if (domains.length > 0 && initialDomainName && !currentDomain) {
changeDomain(domains.find((domain) => domain.name === initialDomainName));
}
}, [domains]);
useEffect(() => {
if (domainName) {
setInitialDomainName(domainName);
}
}, [domainName]);
useEffect(() => {
if (defaultDomainName !== undefined && domains.length > 0) {
changeDomain(domains.find((domain) => domain.name === defaultDomainName));
}
}, [defaultDomainName]);
useEffect(() => {
if (!currentConversation) {
return;
}
const { initMsg, domainId, entityId } = currentConversation;
if (initMsg) {
inputFocus();
if (initMsg === 'CUSTOMIZE' && copilotSendMsg) {
onSendMsg(copilotSendMsg, [], domainId, entityId);
dispatch({
type: 'globalState/setCopilotSendMsg',
payload: '',
});
return;
}
if (initMsg === DEFAULT_CONVERSATION_NAME || initMsg.includes('CUSTOMIZE')) {
sendHelloRsp();
return;
}
onSendMsg(initMsg, [], domainId, entityId);
return;
}
updateHistoryMsg(1);
setPageNo(1);
}, [currentConversation]);
useEffect(() => {
setDefaultEntity(defaultEntityFilter);
}, [defaultEntityFilter]);
useEffect(() => {
if (historyInited) {
const messageContainerEle = document.getElementById('messageContainer');
messageContainerEle?.addEventListener('scroll', handleScroll);
}
return () => {
const messageContainerEle = document.getElementById('messageContainer');
messageContainerEle?.removeEventListener('scroll', handleScroll);
};
}, [historyInited]);
useEffect(() => {
inputFocus();
}, [copilotFullscreen]);
const sendHelloRsp = () => {
setMessageList([
{
id: uuid(),
type: MessageTypeEnum.RECOMMEND_QUESTIONS,
// msg: '您好,请问有什么我能帮您吗?',
type: MessageTypeEnum.TEXT,
msg: defaultDomainName
? `您好,请输入关于${
defaultEntityFilter?.entityName
? `${defaultDomainName?.slice(0, defaultDomainName?.length - 1)}${
defaultEntityFilter?.entityName
}`
: `${defaultDomainName}`
}的问题`
: '您好,请问有什么我能帮您吗?',
},
]);
};
const existInstuctionMsg = (list: HistoryMsgItemType[]) => {
return list.some((msg) => msg.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION);
};
const updateScroll = (list: HistoryMsgItemType[]) => {
if (existInstuctionMsg(list)) {
setMiniProgramLoading(true);
setTimeout(() => {
setMiniProgramLoading(false);
updateMessageContainerScroll();
}, 3000);
} else {
updateMessageContainerScroll();
}
const convertHistoryMsg = (list: HistoryMsgItemType[]) => {
return list.map((item: HistoryMsgItemType) => ({
id: item.questionId,
type:
item.queryResult?.queryMode === MessageTypeEnum.PLUGIN ||
item.queryResult?.queryMode === MessageTypeEnum.WEB_PAGE
? MessageTypeEnum.PLUGIN
: MessageTypeEnum.QUESTION,
msg: item.queryText,
msgData: item.queryResult,
score: item.score,
isHistory: true,
}));
};
const updateHistoryMsg = async (page: number) => {
const res = await getHistoryMsg(page, currentConversation!.chatId, 3);
const { hasNextPage, list } = res.data?.data || { hasNextPage: false, list: [] };
setMessageList([
...list.map((item: HistoryMsgItemType) => ({
id: item.questionId,
type:
item.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION
? MessageTypeEnum.INSTRUCTION
: MessageTypeEnum.QUESTION,
msg: item.queryText,
msgData: item.queryResult,
isHistory: true,
})),
...(page === 1 ? [] : messageList),
]);
const msgList = [...convertHistoryMsg(list), ...(page === 1 ? [] : messageList)];
setMessageList(msgList);
setHasNextPage(hasNextPage);
if (page === 1) {
if (list.length === 0) {
sendHelloRsp();
} else {
setCurrentEntity(list[list.length - 1].queryResult);
}
updateScroll(list);
updateMessageContainerScroll();
setHistoryInited(true);
inputFocus();
}
if (page > 1) {
} else {
const msgEle = document.getElementById(`${messageList[0]?.id}`);
msgEle?.scrollIntoView();
}
@@ -113,31 +208,21 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
},
);
const initDomains = async () => {
try {
const res = await getDomainList();
const domainList = getLeafList(res.data);
setDomains(
[{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...domainList].slice(0, 11),
);
} catch (e) {}
const changeDomain = (domain?: DomainType) => {
setCurrentDomain(domain);
if (onCurrentDomainChange) {
onCurrentDomainChange(domain);
}
};
useEffect(() => {
setChatSdkToken(localStorage.getItem(TOKEN_KEY) || '');
initDomains();
}, []);
useEffect(() => {
if (historyInited) {
const messageContainerEle = document.getElementById('messageContainer');
messageContainerEle?.addEventListener('scroll', handleScroll);
const initDomains = async () => {
const res = await getDomainList();
const domainList = getLeafList(res.data);
setDomains([{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...domainList].slice(0, 11));
if (defaultDomainName !== undefined) {
changeDomain(domainList.find((domain) => domain.name === defaultDomainName));
}
return () => {
const messageContainerEle = document.getElementById('messageContainer');
messageContainerEle?.removeEventListener('scroll', handleScroll);
};
}, [historyInited]);
};
const inputFocus = () => {
if (!isMobile) {
@@ -149,34 +234,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
chatFooterRef.current?.inputBlur();
};
useEffect(() => {
if (!currentConversation) {
return;
}
setCurrentEntity(undefined);
const { initMsg, domainId } = currentConversation;
if (initMsg) {
inputFocus();
if (initMsg === DEFAULT_CONVERSATION_NAME) {
sendHelloRsp();
return;
}
onSendMsg(currentConversation.initMsg, [], domainId);
return;
}
updateHistoryMsg(1);
setPageNo(1);
}, [currentConversation]);
const modifyConversationName = async (name: string) => {
await updateConversationName(name, currentConversation!.chatId);
if (!isMobileMode) {
conversationRef?.current?.updateData();
window.history.replaceState('', '', `?q=${name}&cid=${currentConversation!.chatId}`);
}
};
const onSendMsg = async (msg?: string, list?: MessageItem[], domainId?: number) => {
const onSendMsg = async (
msg?: string,
list?: MessageItem[],
domainId?: number,
entityId?: string,
) => {
const currentMsg = msg || inputMsg;
if (currentMsg.trim() === '') {
setInputMsg('');
@@ -184,8 +247,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
}
const msgDomain = domains.find((item) => currentMsg.includes(item.name));
const certainDomain = currentMsg[0] === '@' && msgDomain;
let domainChanged = false;
if (certainDomain) {
setCurrentDomain(msgDomain.id === -1 ? undefined : msgDomain);
const toDomain = msgDomain.id === -1 ? undefined : msgDomain;
changeDomain(toDomain);
domainChanged = currentDomain?.id !== toDomain?.id;
}
const domainIdValue = domainId || msgDomain?.id || currentDomain?.id;
const msgs = [
@@ -195,6 +262,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
msg: currentMsg,
msgValue: certainDomain ? currentMsg.replace(`@${msgDomain.name}`, '').trim() : currentMsg,
domainId: domainIdValue === -1 ? undefined : domainIdValue,
entityId: entityId || (domainChanged ? undefined : defaultEntity?.entityId),
identityMsg: certainDomain ? getIdentityMsgText(msgDomain) : undefined,
type: MessageTypeEnum.QUESTION,
},
@@ -202,11 +270,6 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
setMessageList(msgs);
updateMessageContainerScroll();
setInputMsg('');
modifyConversationName(currentMsg);
};
const onToggleCollapseBtn = () => {
setConversationCollapsed(!conversationCollapsed);
};
const onInputMsgChange = (value: string) => {
@@ -224,29 +287,38 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
}
};
const onSelectConversation = (conversation: ConversationDetailType, name?: string) => {
const onSelectConversation = (
conversation: ConversationDetailType,
name?: string,
domainId?: number,
entityId?: string,
) => {
if (!isMobileMode) {
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
}
setCurrentConversation({
...conversation,
initMsg: name,
domainId,
entityId,
});
saveConversationToLocal(conversation);
setCurrentDomain(undefined);
};
const onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => {
if (!isMobile) {
conversationRef?.current?.updateData();
}
if (!data) {
return;
}
if (data.queryMode === 'INSTRUCTION') {
if (data.queryMode === 'WEB_PAGE') {
setMessageList([
...messageList.slice(0, messageList.length - 1),
...messageList,
{
id: uuid(),
msg: data.response.name || messageList[messageList.length - 1]?.msg,
type: MessageTypeEnum.INSTRUCTION,
msg: messageList[messageList.length - 1]?.msg,
type: MessageTypeEnum.PLUGIN,
msgData: data,
},
]);
@@ -259,7 +331,6 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
}
updateMessageContainerScroll();
}
setCurrentEntity(data);
};
const onCheckMore = (data: MsgDataType) => {
@@ -268,11 +339,19 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
{
id: uuid(),
msg: data.response.name,
type: MessageTypeEnum.INSTRUCTION,
type: MessageTypeEnum.PLUGIN,
msgData: data,
},
]);
updateMessageContainerScroll();
if (onCheckMoreDetail) {
onCheckMoreDetail();
}
};
const onToggleCollapseBtn = () => {
setConversationCollapsed(!conversationCollapsed);
localStorage.setItem('CONVERSATION_COLLAPSED', `${!conversationCollapsed}`);
};
const getIdentityMsgText = (domain?: DomainType) => {
@@ -281,26 +360,19 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
: '您好,我将尽力帮您解答所有主题相关问题~';
};
const getIdentityMsg = (domain?: DomainType) => {
return {
id: uuid(),
type: MessageTypeEnum.TEXT,
msg: getIdentityMsgText(domain),
};
const onApplyAuth = (domain: string) => {
setApplyAuthDomain(domain);
setApplyAuthVisible(true);
};
const onSelectDomain = (domain: DomainType) => {
const domainValue = currentDomain?.id === domain.id ? undefined : domain;
setCurrentDomain(domainValue);
setCurrentEntity(undefined);
setMessageList([...messageList, getIdentityMsg(domainValue)]);
updateMessageContainerScroll();
const onAddConversation = () => {
conversationRef.current?.onAddConversation();
inputFocus();
};
const chatClass = classNames(styles.chat, {
[styles.mobile]: isMobileMode,
[styles.copilot]: isCopilotMode,
[styles.copilotFullscreen]: copilotFullscreen,
[styles.conversationCollapsed]: conversationCollapsed,
});
@@ -312,6 +384,11 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
<Conversation
currentConversation={currentConversation}
collapsed={conversationCollapsed}
isCopilotMode={isCopilotMode}
defaultDomainName={defaultDomainName}
defaultEntityFilter={defaultEntityFilter}
triggerNewConversation={triggerNewConversation}
onNewConversationTriggered={onNewConversationTriggered}
onSelectConversation={onSelectConversation}
ref={conversationRef}
/>
@@ -325,20 +402,21 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
chatId={currentConversation?.chatId}
isMobileMode={isMobileMode}
conversationCollapsed={conversationCollapsed}
onClickMessageContainer={() => {
inputFocus();
}}
copilotFullscreen={copilotFullscreen}
onClickMessageContainer={inputFocus}
onMsgDataLoaded={onMsgDataLoaded}
onSelectSuggestion={onSendMsg}
onCheckMore={onCheckMore}
onApplyAuth={onApplyAuth}
/>
<ChatFooter
inputMsg={inputMsg}
chatId={currentConversation?.chatId}
domains={domains}
currentDomain={currentDomain}
defaultEntity={defaultEntity}
collapsed={conversationCollapsed}
isMobileMode={isMobileMode}
isCopilotMode={isCopilotMode}
copilotFullscreen={copilotFullscreen}
onToggleCollapseBtn={onToggleCollapseBtn}
onInputMsgChange={onInputMsgChange}
onSendMsg={(msg: string, domainId?: number) => {
@@ -347,9 +425,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
inputBlur();
}
}}
onAddConversation={() => {
conversationRef.current?.onAddConversation();
inputFocus();
onAddConversation={onAddConversation}
onCancelDefaultFilter={() => {
changeDomain(undefined);
if (onCancelCopilotFilter) {
onCancelCopilotFilter();
}
}}
ref={chatFooterRef}
/>
@@ -357,17 +438,6 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
</div>
)}
</div>
{/* {!isMobileMode && (
<RightSection
domains={domains}
currentEntity={currentEntity}
currentDomain={currentDomain}
currentConversation={currentConversation}
onSelectDomain={onSelectDomain}
onSelectConversation={onSelectConversation}
conversationRef={conversationRef}
/>
)} */}
</div>
</div>
);