mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-20 06:34:55 +00:00
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:
@@ -88,7 +88,10 @@ export async function getInitialState(): Promise<{
|
||||
await getToken();
|
||||
}
|
||||
|
||||
const currentUser = await fetchUserInfo();
|
||||
let currentUser: any;
|
||||
if (!window.location.pathname.includes('login')) {
|
||||
currentUser = await fetchUserInfo();
|
||||
}
|
||||
|
||||
if (currentUser) {
|
||||
localStorage.setItem('user', currentUser.staffName);
|
||||
|
||||
@@ -17,11 +17,3 @@ export enum NumericUnit {
|
||||
Million = 'M',
|
||||
Giga = 'G',
|
||||
}
|
||||
|
||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
|
||||
|
||||
export const PAGE_TITLE = '问答对话';
|
||||
|
||||
export const WEB_TITLE = '问答对话 - 超音数';
|
||||
|
||||
export const PLACE_HOLDER = '请输入您的问题';
|
||||
|
||||
@@ -136,9 +136,9 @@ ol {
|
||||
}
|
||||
|
||||
.ant-layout-header {
|
||||
background: linear-gradient(to right, #153d8f, #0a276d);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
background: linear-gradient(to right, #153d8f, #0a276d) !important;
|
||||
background-color: rgba(0, 0, 0, 0.2) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -242,15 +242,16 @@ ol {
|
||||
.ant-tag {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semantic-graph-toolbar {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
width: 190px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.g6-component-tooltip {
|
||||
p {
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ export default {
|
||||
'menu.exception.not-find': '404',
|
||||
'menu.exception.server-error': '500',
|
||||
'menu.semanticModel': '模型管理',
|
||||
'menu.metric': '指标市场',
|
||||
'menu.chatSetting': '问答设置',
|
||||
'menu.chatPlugin': '问答插件',
|
||||
'menu.login': '登录',
|
||||
'menu.chat': '问答对话',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { getTextWidth, groupByColumn } from '@/utils/utils';
|
||||
import { getTextWidth, groupByColumn, isMobile } from '@/utils/utils';
|
||||
import { AutoComplete, Select, Tag, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
@@ -9,20 +9,23 @@ import { searchRecommend } from 'supersonic-chat-sdk';
|
||||
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
|
||||
import styles from './style.less';
|
||||
import { PLACE_HOLDER } from '../constants';
|
||||
import { DomainType } from '../type';
|
||||
import { DefaultEntityType, DomainType } from '../type';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||
|
||||
type Props = {
|
||||
inputMsg: string;
|
||||
chatId?: number;
|
||||
currentDomain?: DomainType;
|
||||
defaultEntity?: DefaultEntityType;
|
||||
isCopilotMode?: boolean;
|
||||
copilotFullscreen?: boolean;
|
||||
domains: DomainType[];
|
||||
isMobileMode?: boolean;
|
||||
collapsed: boolean;
|
||||
onToggleCollapseBtn: () => void;
|
||||
onInputMsgChange: (value: string) => void;
|
||||
onSendMsg: (msg: string, domainId?: number) => void;
|
||||
onAddConversation: () => void;
|
||||
onCancelDefaultFilter: () => void;
|
||||
};
|
||||
|
||||
const { OptGroup, Option } = Select;
|
||||
@@ -42,13 +45,16 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
inputMsg,
|
||||
chatId,
|
||||
currentDomain,
|
||||
defaultEntity,
|
||||
domains,
|
||||
isMobileMode,
|
||||
collapsed,
|
||||
isCopilotMode,
|
||||
copilotFullscreen,
|
||||
onToggleCollapseBtn,
|
||||
onInputMsgChange,
|
||||
onSendMsg,
|
||||
onAddConversation,
|
||||
onCancelDefaultFilter,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -95,7 +101,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
|
||||
const getStepOptions = (recommends: any[]) => {
|
||||
const data = groupByColumn(recommends, 'domainName');
|
||||
return isMobileMode && recommends.length > 6
|
||||
return isMobile && recommends.length > 6
|
||||
? Object.keys(data)
|
||||
.slice(0, 4)
|
||||
.reduce((result, key) => {
|
||||
@@ -135,7 +141,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
const { msgValue, domainId } = processMsg(msg, domains);
|
||||
const res = await searchRecommend(msgValue.trim(), chatId, domainId || domain?.id);
|
||||
const domainIdValue = domainId || domain?.id;
|
||||
const res = await searchRecommend(msgValue.trim(), chatId, domainIdValue);
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -150,7 +157,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
}
|
||||
setOpen(recommends.length > 0);
|
||||
};
|
||||
return debounce(getAssociateWords, 20);
|
||||
return debounce(getAssociateWords, 200);
|
||||
}, []);
|
||||
|
||||
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
|
||||
@@ -222,7 +229,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
};
|
||||
|
||||
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
|
||||
[styles.mobile]: isMobileMode,
|
||||
[styles.mobile]: isMobile,
|
||||
[styles.domainOptions]: domainOptions.length > 0,
|
||||
});
|
||||
|
||||
@@ -238,7 +245,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
};
|
||||
|
||||
const chatFooterClass = classNames(styles.chatFooter, {
|
||||
[styles.mobile]: isMobileMode,
|
||||
[styles.mobile]: isMobile,
|
||||
[styles.defaultCopilotMode]: isCopilotMode && !copilotFullscreen,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -255,6 +263,33 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
/>
|
||||
</Tooltip>
|
||||
<div className={styles.composerInputWrapper}>
|
||||
{currentDomain && (
|
||||
<div className={styles.currentDomain}>
|
||||
<div className={styles.currentDomainName}>
|
||||
输入联想与问题回复将限定于:“
|
||||
<span className={styles.quoteText}>
|
||||
主题域【{currentDomain.name}】
|
||||
{defaultEntity && (
|
||||
<>
|
||||
<span>,</span>
|
||||
<span>{`${currentDomain.name.slice(
|
||||
0,
|
||||
currentDomain.name.length - 1,
|
||||
)}【`}</span>
|
||||
<span className={styles.entityName} title={defaultEntity.entityName}>
|
||||
{defaultEntity.entityName}
|
||||
</span>
|
||||
<span>】</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
”
|
||||
</div>
|
||||
<div className={styles.cancelDomain} onClick={onCancelDefaultFilter}>
|
||||
取消限定
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AutoComplete
|
||||
className={styles.composerInput}
|
||||
placeholder={
|
||||
@@ -265,7 +300,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
value={inputMsg}
|
||||
onChange={onInputMsgChange}
|
||||
onSelect={onSelect}
|
||||
autoFocus={!isMobileMode}
|
||||
autoFocus={!isMobile}
|
||||
backfill
|
||||
ref={inputRef}
|
||||
id="chatInput"
|
||||
@@ -332,7 +367,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
? 'blue'
|
||||
: option.schemaElementType === SemanticTypeEnum.VALUE
|
||||
? 'geekblue'
|
||||
: 'orange'
|
||||
: 'cyan'
|
||||
}
|
||||
>
|
||||
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
margin-right: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
&.defaultCopilotMode {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
height: 46px;
|
||||
@@ -38,8 +42,53 @@
|
||||
}
|
||||
|
||||
.composerInputWrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.currentDomain {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 30px);
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
overflow-x: auto;
|
||||
color: var(--text-color-third);
|
||||
white-space: nowrap;
|
||||
background: #f4f6f5;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
|
||||
.currentDomainName {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
|
||||
.entityName {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelDomain {
|
||||
padding: 0 6px;
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--text-color-fourth);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color-fourth);
|
||||
border-color: var(--text-color-fifth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composerInput {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -191,3 +240,7 @@
|
||||
.semanticType {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.quoteText {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
|
||||
@@ -9,17 +9,22 @@ import {
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useLocation } from 'umi';
|
||||
import ConversationModal from './ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from '../service';
|
||||
import ConversationModal from './components/ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from './service';
|
||||
import styles from './style.less';
|
||||
import { ConversationDetailType } from '../type';
|
||||
import { ConversationDetailType, DefaultEntityType } from './type';
|
||||
import { DEFAULT_CONVERSATION_NAME } from './constants';
|
||||
import moment from 'moment';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { DEFAULT_CONVERSATION_NAME } from '@/common/constants';
|
||||
|
||||
type Props = {
|
||||
currentConversation?: ConversationDetailType;
|
||||
collapsed?: boolean;
|
||||
isCopilotMode?: boolean;
|
||||
defaultDomainName?: string;
|
||||
defaultEntityFilter?: DefaultEntityType;
|
||||
triggerNewConversation?: boolean;
|
||||
onNewConversationTriggered?: () => void;
|
||||
onSelectConversation: (
|
||||
conversation: ConversationDetailType,
|
||||
name?: string,
|
||||
@@ -29,7 +34,16 @@ type Props = {
|
||||
};
|
||||
|
||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
{ currentConversation, collapsed, onSelectConversation },
|
||||
{
|
||||
currentConversation,
|
||||
collapsed,
|
||||
isCopilotMode,
|
||||
defaultDomainName,
|
||||
defaultEntityFilter,
|
||||
triggerNewConversation,
|
||||
onNewConversationTriggered,
|
||||
onSelectConversation,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const location = useLocation();
|
||||
@@ -47,7 +61,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
const updateData = async () => {
|
||||
const { data } = await getAllConversations();
|
||||
const conversationList = data || [];
|
||||
setConversations(conversationList);
|
||||
setConversations(conversationList.slice(0, 500));
|
||||
return conversationList;
|
||||
};
|
||||
|
||||
@@ -71,6 +85,20 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerNewConversation) {
|
||||
const conversationName =
|
||||
defaultEntityFilter?.entityName && window.location.pathname.includes('detail')
|
||||
? defaultEntityFilter.entityName
|
||||
: defaultDomainName;
|
||||
onAddConversation({ name: conversationName, type: 'CUSTOMIZE' });
|
||||
onNewConversationTriggered?.();
|
||||
}
|
||||
}, [triggerNewConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerNewConversation) {
|
||||
return;
|
||||
}
|
||||
if (q && cid === undefined && window.location.href.includes('/workbench/chat')) {
|
||||
onAddConversation({ name: q, domainId: domainId ? +domainId : undefined, entityId });
|
||||
} else {
|
||||
@@ -114,6 +142,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
|
||||
const conversationClass = classNames(styles.conversation, {
|
||||
[styles.collapsed]: collapsed,
|
||||
[styles.copilotMode]: isCopilotMode,
|
||||
});
|
||||
|
||||
const convertTime = (date: string) => {
|
||||
@@ -1,149 +0,0 @@
|
||||
.conversation {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
height: 100vh !important;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid var(--border-color-base);
|
||||
|
||||
.leftSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.searchConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 9px 10px;
|
||||
|
||||
.searchIcon {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.searchTask {
|
||||
font-size: 13px;
|
||||
background-color: #f5f5f5;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
|
||||
:global {
|
||||
.ant-input {
|
||||
font-size: 13px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
height: calc(100vh - 50px);
|
||||
padding: 2px 8px 0;
|
||||
overflow-y: auto;
|
||||
.conversationItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.conversationIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.conversationContent {
|
||||
width: 100%;
|
||||
|
||||
.topTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversationName {
|
||||
width: 150px;
|
||||
margin-right: 2px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conversationTime {
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&.activeConversationItem {
|
||||
background-color: var(--light-blue-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
margin-top: 20px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.operateItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
|
||||
.operateIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.operateLabel {
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.operateLabel {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
|
||||
.leftSection {
|
||||
.searchConversation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import { isEqual } from 'lodash';
|
||||
import { ChatItem } from 'supersonic-chat-sdk';
|
||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||
import { MessageItem, MessageTypeEnum } from './type';
|
||||
import Plugin from './components/Plugin';
|
||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||
import styles from './style.less';
|
||||
import RecommendQuestions from './components/RecommendQuestions';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
@@ -13,10 +14,16 @@ type Props = {
|
||||
messageList: MessageItem[];
|
||||
isMobileMode?: boolean;
|
||||
conversationCollapsed: boolean;
|
||||
copilotFullscreen?: boolean;
|
||||
onClickMessageContainer: () => void;
|
||||
onMsgDataLoaded: (data: MsgDataType, questionId: string | number) => void;
|
||||
onSelectSuggestion: (value: string) => void;
|
||||
onMsgDataLoaded: (
|
||||
data: MsgDataType,
|
||||
questionId: string | number,
|
||||
question: string,
|
||||
valid: boolean,
|
||||
) => void;
|
||||
onCheckMore: (data: MsgDataType) => void;
|
||||
onApplyAuth: (domain: string) => void;
|
||||
};
|
||||
|
||||
const MessageContainer: React.FC<Props> = ({
|
||||
@@ -25,9 +32,11 @@ const MessageContainer: React.FC<Props> = ({
|
||||
messageList,
|
||||
isMobileMode,
|
||||
conversationCollapsed,
|
||||
copilotFullscreen,
|
||||
onClickMessageContainer,
|
||||
onMsgDataLoaded,
|
||||
onSelectSuggestion,
|
||||
onCheckMore,
|
||||
onApplyAuth,
|
||||
}) => {
|
||||
const [triggerResize, setTriggerResize] = useState(false);
|
||||
|
||||
@@ -38,10 +47,6 @@ const MessageContainer: React.FC<Props> = ({
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
}, [conversationCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => {
|
||||
@@ -49,6 +54,15 @@ const MessageContainer: React.FC<Props> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
}, [conversationCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
updateMessageContainerScroll();
|
||||
}, [copilotFullscreen]);
|
||||
|
||||
const getFollowQuestions = (index: number) => {
|
||||
const followQuestions: string[] = [];
|
||||
const currentMsg = messageList[index];
|
||||
@@ -63,7 +77,7 @@ const MessageContainer: React.FC<Props> = ({
|
||||
const currentMsgEntityId = currentMsgData?.entityInfo?.entityId;
|
||||
|
||||
if (
|
||||
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.INSTRUCTION) &&
|
||||
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.PLUGIN) &&
|
||||
!!currentMsgDomainId &&
|
||||
msgDomainId === currentMsgDomainId &&
|
||||
msgEntityId === currentMsgEntityId &&
|
||||
@@ -77,19 +91,38 @@ const MessageContainer: React.FC<Props> = ({
|
||||
return followQuestions;
|
||||
};
|
||||
|
||||
const getFilters = (domainId?: number, entityId?: string) => {
|
||||
if (!domainId || !entityId) {
|
||||
return undefined;
|
||||
}
|
||||
return [
|
||||
{
|
||||
value: entityId,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
||||
<div className={styles.messageList}>
|
||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||
const { id: msgId, domainId, type, msg, msgValue, identityMsg, msgData } = msgItem;
|
||||
const {
|
||||
id: msgId,
|
||||
domainId,
|
||||
entityId,
|
||||
type,
|
||||
msg,
|
||||
msgValue,
|
||||
identityMsg,
|
||||
msgData,
|
||||
score,
|
||||
isHistory,
|
||||
} = msgItem;
|
||||
|
||||
const followQuestions = getFollowQuestions(index);
|
||||
|
||||
return (
|
||||
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
||||
{type === MessageTypeEnum.RECOMMEND_QUESTIONS && (
|
||||
<RecommendQuestions onSelectQuestion={onSelectSuggestion} />
|
||||
)}
|
||||
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
||||
{type === MessageTypeEnum.QUESTION && (
|
||||
<>
|
||||
@@ -97,16 +130,35 @@ const MessageContainer: React.FC<Props> = ({
|
||||
{identityMsg && <Text position="left" data={identityMsg} />}
|
||||
<ChatItem
|
||||
msg={msgValue || msg || ''}
|
||||
followQuestions={followQuestions}
|
||||
msgData={msgData}
|
||||
conversationId={chatId}
|
||||
domainId={domainId}
|
||||
filter={getFilters(domainId, entityId)}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
onMsgDataLoaded={(data: MsgDataType) => {
|
||||
onMsgDataLoaded(data, msgId);
|
||||
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
|
||||
}}
|
||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === MessageTypeEnum.PLUGIN && (
|
||||
<>
|
||||
<Plugin
|
||||
id={msgId}
|
||||
followQuestions={followQuestions}
|
||||
data={msgData!}
|
||||
scoreValue={score}
|
||||
msg={msgValue || msg || ''}
|
||||
isHistory={isHistory}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
isMobileMode={isMobileMode}
|
||||
onReportLoaded={(height: number) => {
|
||||
updateMessageContainerScroll(true, height);
|
||||
}}
|
||||
onCheckMore={onCheckMore}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -122,7 +174,8 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
||||
if (
|
||||
prevProps.id === nextProps.id &&
|
||||
isEqual(prevProps.messageList, nextProps.messageList) &&
|
||||
prevProps.conversationCollapsed === nextProps.conversationCollapsed
|
||||
prevProps.conversationCollapsed === nextProps.conversationCollapsed &&
|
||||
prevProps.copilotFullscreen === nextProps.copilotFullscreen
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { DomainType } from '../../type';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
domain: DomainType;
|
||||
};
|
||||
|
||||
const DomainInfo: React.FC<Props> = ({ domain }) => {
|
||||
return (
|
||||
<div className={styles.context}>
|
||||
<div className={styles.title}>相关信息</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>主题域:</span>
|
||||
<span className={styles.fieldValue}>{domain.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainInfo;
|
||||
@@ -1,59 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import styles from './style.less';
|
||||
import type { ChatContextType, EntityInfoType } from 'supersonic-chat-sdk';
|
||||
|
||||
type Props = {
|
||||
chatContext: ChatContextType;
|
||||
entityInfo?: EntityInfoType;
|
||||
};
|
||||
|
||||
const Context: React.FC<Props> = ({ chatContext, entityInfo }) => {
|
||||
const { domainName, metrics, dateInfo, dimensionFilters } = chatContext;
|
||||
|
||||
return (
|
||||
<div className={styles.context}>
|
||||
<div className={styles.title}>相关信息</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>主题域:</span>
|
||||
<span className={styles.fieldValue}>{domainName}</span>
|
||||
</div>
|
||||
{dateInfo && (
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>时间范围:</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{dateInfo.text ||
|
||||
`近${moment(dateInfo.endDate).diff(moment(dateInfo.startDate), 'days') + 1}天`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{metrics && metrics.length > 0 && (
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>指标:</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{metrics.map((metric) => metric.name).join('、')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{dimensionFilters &&
|
||||
dimensionFilters.length > 0 &&
|
||||
!(entityInfo?.dimensions && entityInfo.dimensions.length > 0) && (
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.fieldName}>筛选条件:</div>
|
||||
<div className={styles.filterValues}>
|
||||
{dimensionFilters.map((filter) => {
|
||||
return (
|
||||
<div className={styles.filterItem} key={filter.name}>
|
||||
{filter.name}:{filter.value}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Context;
|
||||
@@ -1,72 +0,0 @@
|
||||
.context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 10px 0;
|
||||
border-top: 1px solid #ccc;
|
||||
|
||||
.title {
|
||||
margin-bottom: 22px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
max-height: 350px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow-y: auto;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.columnLayout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterSection {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fieldName {
|
||||
margin-right: 6px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filterValues {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
column-gap: 4px;
|
||||
row-gap: 4px;
|
||||
|
||||
.filterItem {
|
||||
padding: 2px 12px;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: var(--body-background);
|
||||
border-radius: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { DomainType } from '../../type';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
domains: DomainType[];
|
||||
currentDomain?: DomainType;
|
||||
onSelectDomain: (domain: DomainType) => void;
|
||||
};
|
||||
|
||||
const Domains: React.FC<Props> = ({ domains, currentDomain, onSelectDomain }) => {
|
||||
return (
|
||||
<div className={styles.domains}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={styles.title}>主题列表</div>
|
||||
<div className={styles.subTitle}>(可在输入框@)</div>
|
||||
</div>
|
||||
<div className={styles.domainList}>
|
||||
{domains
|
||||
.filter((domain) => domain.id !== -1)
|
||||
.map((domain) => {
|
||||
const domainItemClass = classNames(styles.domainItem, {
|
||||
[styles.activeDomainItem]: currentDomain?.id === domain.id,
|
||||
});
|
||||
return (
|
||||
<div key={domain.id}>
|
||||
<div
|
||||
className={domainItemClass}
|
||||
onClick={() => {
|
||||
onSelectDomain(domain);
|
||||
}}
|
||||
>
|
||||
{/* <IconFont type="icon-yinleku" className={styles.domainIcon} /> */}
|
||||
<div className={styles.domainName}>{domain.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Domains;
|
||||
@@ -1,70 +0,0 @@
|
||||
.domains {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ccc;
|
||||
|
||||
.titleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title {
|
||||
padding-left: 10px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
}
|
||||
|
||||
.domainList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.domainItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
.loadingIcon {
|
||||
margin-right: 6px;
|
||||
color: var(--text-color-fifth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.arrowIcon {
|
||||
margin-right: 6px;
|
||||
color: var(--text-color-fifth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.domainIcon {
|
||||
margin-right: 6px;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.domainName {
|
||||
width: 150px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-secondary);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--link-hover-bg-color);
|
||||
}
|
||||
|
||||
&.activeDomainItem {
|
||||
background-color: var(--link-hover-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { getFormattedValueData } from '@/utils/utils';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import styles from './style.less';
|
||||
import type { EntityInfoType, MsgDataType } from 'supersonic-chat-sdk';
|
||||
|
||||
type Props = {
|
||||
currentEntity: MsgDataType;
|
||||
};
|
||||
|
||||
const Introduction: React.FC<Props> = ({ currentEntity }) => {
|
||||
const { entityInfo } = currentEntity;
|
||||
const { dimensions, metrics } = entityInfo || ({} as EntityInfoType);
|
||||
|
||||
return (
|
||||
<div className={styles.introduction}>
|
||||
{dimensions
|
||||
?.filter((dimension) => !dimension.bizName.includes('photo'))
|
||||
.map((dimension) => {
|
||||
return (
|
||||
<div className={styles.field} key={dimension.name}>
|
||||
<span className={styles.fieldName}>{dimension.name}:</span>
|
||||
{dimension.bizName.includes('photo') ? (
|
||||
<img width={40} height={40} src={dimension.value} alt="" />
|
||||
) : (
|
||||
<span className={styles.fieldValue}>
|
||||
{dimension.bizName.includes('publish_time')
|
||||
? moment(dimension.value).format('YYYY-MM-DD')
|
||||
: dimension.value}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{metrics?.map((metric) => (
|
||||
<div className={styles.field} key={metric.name}>
|
||||
<span className={styles.fieldName}>{metric.name}:</span>
|
||||
<span className={styles.fieldValue}>{getFormattedValueData(metric.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Introduction;
|
||||
@@ -1,63 +0,0 @@
|
||||
.introduction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 10px 4px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 22px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
max-height: 350px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow-y: auto;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.columnLayout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldName {
|
||||
margin-right: 6px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
color: var(--text-color);
|
||||
|
||||
&.switchField {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.dimensionFieldValue {
|
||||
max-width: 90px;
|
||||
// white-space: nowrap;
|
||||
}
|
||||
|
||||
&.mainNameFieldValue {
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import Context from './Context';
|
||||
import Introduction from './Introduction';
|
||||
import styles from './style.less';
|
||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||
import Domains from './Domains';
|
||||
import { ConversationDetailType, DomainType } from '../type';
|
||||
import DomainInfo from './Context/DomainInfo';
|
||||
import Conversation from '../Conversation';
|
||||
|
||||
type Props = {
|
||||
domains: DomainType[];
|
||||
currentEntity?: MsgDataType;
|
||||
currentConversation?: ConversationDetailType;
|
||||
currentDomain?: DomainType;
|
||||
conversationRef: any;
|
||||
onSelectConversation: (conversation: ConversationDetailType, name?: string) => void;
|
||||
onSelectDomain: (domain: DomainType) => void;
|
||||
};
|
||||
|
||||
const RightSection: React.FC<Props> = ({
|
||||
domains,
|
||||
currentEntity,
|
||||
currentDomain,
|
||||
currentConversation,
|
||||
conversationRef,
|
||||
onSelectConversation,
|
||||
onSelectDomain,
|
||||
}) => {
|
||||
const rightSectionClass = classNames(styles.rightSection, {
|
||||
[styles.external]: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={rightSectionClass}>
|
||||
<Conversation
|
||||
currentConversation={currentConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
ref={conversationRef}
|
||||
/>
|
||||
{currentDomain && !currentEntity && (
|
||||
<div className={styles.entityInfo}>
|
||||
<DomainInfo domain={currentDomain} />
|
||||
</div>
|
||||
)}
|
||||
{!!currentEntity?.chatContext?.domainId && (
|
||||
<div className={styles.entityInfo}>
|
||||
<Context chatContext={currentEntity.chatContext} entityInfo={currentEntity.entityInfo} />
|
||||
<Introduction currentEntity={currentEntity} />
|
||||
</div>
|
||||
)}
|
||||
{domains && domains.length > 0 && (
|
||||
<Domains domains={domains} currentDomain={currentDomain} onSelectDomain={onSelectDomain} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightSection;
|
||||
@@ -1,18 +0,0 @@
|
||||
.rightSection {
|
||||
width: 225px;
|
||||
height: calc(100vh - 48px);
|
||||
margin-right: 12px;
|
||||
padding-bottom: 10px;
|
||||
overflow-y: auto;
|
||||
|
||||
.entityInfo {
|
||||
margin-top: 20px;
|
||||
|
||||
.topInfo {
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-color-third);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import styles from './style.less';
|
||||
|
||||
const LeftAvatar = () => {
|
||||
const CopilotAvatar = () => {
|
||||
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
|
||||
};
|
||||
|
||||
export default LeftAvatar;
|
||||
export default CopilotAvatar;
|
||||
@@ -34,7 +34,14 @@ const Message: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className={messageClass} style={{ width }}>
|
||||
{!!domainName && <div className={styles.domainName}>{domainName}</div>}
|
||||
{/* <div className={styles.messageTitleBar}>
|
||||
{!!domainName && <div className={styles.domainName}>{domainName}</div>}
|
||||
{position === 'left' && leftTitle && (
|
||||
<div className={styles.messageTopBar} title={leftTitle}>
|
||||
({leftTitle})
|
||||
</div>
|
||||
)}
|
||||
</div> */}
|
||||
<div className={styles.messageContent}>
|
||||
<div className={styles.messageBody}>
|
||||
<div
|
||||
@@ -44,11 +51,11 @@ const Message: React.FC<Props> = ({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{position === 'left' && question && (
|
||||
{/* {position === 'left' && question && (
|
||||
<div className={styles.messageTopBar} title={leftTitle}>
|
||||
{leftTitle}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const TRANS_30_REPORT_DASHBOARD_ID = '1165';
|
||||
|
||||
export const HEAT_COMPARE_REPORT_DASHBOARD_ID = '1148';
|
||||
|
||||
export const INSIGHTS_DETAIL_ID = 'DETAIL';
|
||||
export const INSIGHTS_ID = 'INSIGHT';
|
||||
@@ -0,0 +1,195 @@
|
||||
import { isProd } from '@/utils/utils';
|
||||
import { MsgDataType } from 'supersonic-chat-sdk';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import Message from '../Message';
|
||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||
import styles from './style.less';
|
||||
import LeftAvatar from '../CopilotAvatar';
|
||||
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
||||
import { updateQAFeedback } from '../../service';
|
||||
|
||||
type Props = {
|
||||
id: string | number;
|
||||
followQuestions?: string[];
|
||||
data: MsgDataType;
|
||||
scoreValue?: number;
|
||||
msg: string;
|
||||
isHistory?: boolean;
|
||||
isLastMessage: boolean;
|
||||
isMobileMode?: boolean;
|
||||
onReportLoaded: (height: number) => void;
|
||||
onCheckMore: (data: MsgDataType) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_HEIGHT = 800;
|
||||
|
||||
const Plugin: React.FC<Props> = ({
|
||||
id,
|
||||
followQuestions,
|
||||
data,
|
||||
scoreValue,
|
||||
msg,
|
||||
isHistory,
|
||||
isLastMessage,
|
||||
isMobileMode,
|
||||
onReportLoaded,
|
||||
onCheckMore,
|
||||
}) => {
|
||||
const {
|
||||
name,
|
||||
webPage: { url, params },
|
||||
} = data.response || {};
|
||||
|
||||
const [pluginUrl, setPluginUrl] = useState('');
|
||||
const [height, setHeight] = useState(DEFAULT_HEIGHT);
|
||||
const [score, setScore] = useState(scoreValue || 0);
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const messageData = event.data;
|
||||
const { type, payload } = messageData;
|
||||
if (type === 'changeMiniProgramContainerSize') {
|
||||
const { msgId, height } = payload;
|
||||
if (`${msgId}` === `${id}`) {
|
||||
setHeight(height);
|
||||
updateMessageContainerScroll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (messageData === 'storyResize') {
|
||||
const ifr: any = document.getElementById(`reportIframe_${id}`);
|
||||
const iDoc = ifr.contentDocument || ifr.document || ifr.contentWindow;
|
||||
setTimeout(() => {
|
||||
setHeight(isProd() ? calcPageHeight(iDoc) : DEFAULT_HEIGHT);
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, [handleMessage]);
|
||||
|
||||
function calcPageHeight(doc: any) {
|
||||
const titleAreaEl = doc.getElementById('titleArea');
|
||||
const titleAreaHeight = Math.max(
|
||||
titleAreaEl?.clientHeight || 0,
|
||||
titleAreaEl?.scrollHeight || 0,
|
||||
);
|
||||
const dashboardGridEl = doc.getElementsByClassName('dashboardGrid')?.[0];
|
||||
const dashboardGridHeight = Math.max(
|
||||
dashboardGridEl?.clientHeight || 0,
|
||||
dashboardGridEl?.scrollHeight || 0,
|
||||
);
|
||||
return Math.max(titleAreaHeight + dashboardGridHeight + 10, DEFAULT_HEIGHT);
|
||||
}
|
||||
|
||||
const initData = () => {
|
||||
const heightValue =
|
||||
params?.find((option: any) => option.paramType === 'FORWARD' && option.key === 'height')
|
||||
?.value || DEFAULT_HEIGHT;
|
||||
setHeight(heightValue);
|
||||
let urlValue = url;
|
||||
const valueParams = (params || [])
|
||||
.filter((option: any) => option.paramType !== 'FORWARD')
|
||||
.reduce((result: any, item: any) => {
|
||||
result[item.key] = item.value;
|
||||
return result;
|
||||
}, {});
|
||||
if (urlValue.includes('?type=dashboard') || urlValue.includes('?type=widget')) {
|
||||
const filterData = encodeURIComponent(
|
||||
JSON.stringify(
|
||||
urlValue.includes('dashboard')
|
||||
? {
|
||||
global: valueParams,
|
||||
}
|
||||
: {
|
||||
local: valueParams,
|
||||
},
|
||||
),
|
||||
);
|
||||
urlValue = urlValue.replace(
|
||||
'?',
|
||||
`?miniProgram=true&reportName=${name}&filterData=${filterData}&`,
|
||||
);
|
||||
urlValue =
|
||||
!isProd() && !urlValue.includes('http') ? `http://s2.tmeoa.com${urlValue}` : urlValue;
|
||||
} else {
|
||||
const params = Object.keys(valueParams || {}).map((key) => `${key}=${valueParams[key]}`);
|
||||
if (params.length > 0) {
|
||||
if (url.includes('?')) {
|
||||
urlValue = urlValue.replace('?', `?${params.join('&')}&`);
|
||||
} else {
|
||||
urlValue = `${urlValue}?${params.join('&')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReportLoaded(heightValue + 190);
|
||||
setPluginUrl(urlValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
const reportClass = classNames(styles.report, {
|
||||
[styles.mobileMode]: isMobileMode,
|
||||
});
|
||||
|
||||
const like = () => {
|
||||
setScore(5);
|
||||
updateQAFeedback(data.queryId, 5);
|
||||
};
|
||||
|
||||
const dislike = () => {
|
||||
setScore(1);
|
||||
updateQAFeedback(data.queryId, 1);
|
||||
};
|
||||
|
||||
const likeClass = classNames(styles.like, {
|
||||
[styles.feedbackActive]: score === 5,
|
||||
});
|
||||
|
||||
const dislikeClass = classNames(styles.dislike, {
|
||||
[styles.feedbackActive]: score === 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={reportClass}>
|
||||
<LeftAvatar />
|
||||
<div className={styles.msgContent}>
|
||||
<Message
|
||||
position="left"
|
||||
width="100%"
|
||||
height={height}
|
||||
bubbleClassName={styles.reportBubble}
|
||||
domainName={data.chatContext?.domainName}
|
||||
question={msg}
|
||||
followQuestions={followQuestions}
|
||||
>
|
||||
<iframe
|
||||
id={`reportIframe_${id}`}
|
||||
src={pluginUrl}
|
||||
className={styles.reportContent}
|
||||
style={{ height }}
|
||||
allowFullScreen
|
||||
/>
|
||||
</Message>
|
||||
{isLastMessage && (
|
||||
<div className={styles.tools}>
|
||||
<div className={styles.feedback}>
|
||||
<div>这个回答正确吗?</div>
|
||||
<LikeOutlined className={likeClass} onClick={like} />
|
||||
<DislikeOutlined className={dislikeClass} onClick={dislike} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Plugin;
|
||||
@@ -0,0 +1,69 @@
|
||||
.report {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
&.reportLoading {
|
||||
position: absolute;
|
||||
bottom: 10000px;
|
||||
}
|
||||
|
||||
.msgContent {
|
||||
width: 100%;
|
||||
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
column-gap: 4px;
|
||||
|
||||
.feedback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-third);
|
||||
column-gap: 6px;
|
||||
|
||||
.like {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.feedbackActive {
|
||||
color: rgb(234, 197, 79);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobileMode {
|
||||
.msgContent {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.reportBubble {
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
background-color: #fff !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.reportContent {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.saveReport {
|
||||
width: fit-content;
|
||||
margin-top: 12px;
|
||||
padding: 4px 16px;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--border-color-base);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import LeftAvatar from '../LeftAvatar';
|
||||
import LeftAvatar from '../CopilotAvatar';
|
||||
import Message from '../Message';
|
||||
import styles from './style.less';
|
||||
import { queryRecommendQuestions } from '../../service';
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { querySuggestion } from '../../service';
|
||||
import { SuggestionType } from '../../type';
|
||||
import Message from '../Message';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
domainId: number;
|
||||
onSelectSuggestion: (value: string) => void;
|
||||
};
|
||||
|
||||
const Suggestion: React.FC<Props> = ({ domainId, onSelectSuggestion }) => {
|
||||
const [data, setData] = useState<SuggestionType>({ dimensions: [], metrics: [] });
|
||||
const { metrics } = data;
|
||||
|
||||
const initData = async () => {
|
||||
const res = await querySuggestion(domainId);
|
||||
setData({
|
||||
dimensions: res.data.dimensions.slice(0, 5),
|
||||
metrics: res.data.metrics.slice(0, 5),
|
||||
});
|
||||
updateMessageContainerScroll();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.suggestion}>
|
||||
<Message position="left" bubbleClassName={styles.suggestionMsg}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowTitle}>您可能还想问以下指标:</div>
|
||||
<div className={styles.rowContent}>
|
||||
{metrics.map((metric, index) => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={styles.contentItemName}
|
||||
onClick={() => {
|
||||
onSelectSuggestion(metric.name);
|
||||
}}
|
||||
>
|
||||
{metric.name}
|
||||
</span>
|
||||
{index !== metrics.length - 1 && <span>、</span>}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Message>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Suggestion;
|
||||
@@ -1,37 +0,0 @@
|
||||
.suggestion {
|
||||
margin-left: 46px;
|
||||
|
||||
.suggestionMsg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px !important;
|
||||
row-gap: 12px;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
row-gap: 12px;
|
||||
|
||||
.rowTitle {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
.rowContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
row-gap: 12px;
|
||||
|
||||
.contentItemName {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import LeftAvatar from './LeftAvatar';
|
||||
import LeftAvatar from './CopilotAvatar';
|
||||
import Message from './Message';
|
||||
import styles from './style.less';
|
||||
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
.message {
|
||||
.domainName {
|
||||
margin-bottom: 2px;
|
||||
margin-left: 4px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
.messageTitleBar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 6px;
|
||||
column-gap: 10px;
|
||||
|
||||
.domainName {
|
||||
margin-left: 4px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.messageTopBar {
|
||||
position: relative;
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.messageContent {
|
||||
display: flex;
|
||||
@@ -11,18 +27,6 @@
|
||||
|
||||
.messageBody {
|
||||
width: 100%;
|
||||
|
||||
.messageTopBar {
|
||||
max-width: 90%;
|
||||
margin: 0 16px;
|
||||
padding: 12px 0 8px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -88,7 +92,7 @@
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
@@ -229,7 +233,6 @@
|
||||
|
||||
.typingBubble {
|
||||
width: fit-content;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.quote {
|
||||
|
||||
@@ -27,10 +27,12 @@ export const SEMANTIC_TYPE_MAP = {
|
||||
[SemanticTypeEnum.VALUE]: '维度值',
|
||||
};
|
||||
|
||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话'
|
||||
export const CHAT_TITLE = '';
|
||||
|
||||
export const WEB_TITLE = '问答对话'
|
||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
|
||||
|
||||
export const CHAT_TITLE = '问答'
|
||||
export const PAGE_TITLE = '问答对话';
|
||||
|
||||
export const PLACE_HOLDER = '请输入您的问题'
|
||||
export const WEB_TITLE = '问答对话';
|
||||
|
||||
export const PLACE_HOLDER = '请输入您的问题';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -24,10 +24,19 @@ export function getAllConversations() {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/getAll`);
|
||||
}
|
||||
|
||||
export function getMiniProgramList(entityId: string, domainId: number) {
|
||||
return request<Result<any>>(
|
||||
`${prefix}/chat/plugin/extend/getAvailablePlugin/${entityId}/${domainId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
skipErrorHandler: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getDomainList() {
|
||||
return request<Result<DomainType[]>>(`${prefix}/chat/conf/domainList/view`, {
|
||||
method: 'GET',
|
||||
skipErrorHandler: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +49,12 @@ export function updateQAFeedback(questionId: number, score: number) {
|
||||
);
|
||||
}
|
||||
|
||||
export function queryMetricSuggestion(domainId: number) {
|
||||
return request<Result<any>>(`${prefix}/chat/recommend/metric/${domainId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function querySuggestion(domainId: number) {
|
||||
return request<Result<any>>(`${prefix}/chat/recommend/${domainId}`, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100vw - 260px);
|
||||
height: calc(100vh - 48px);
|
||||
padding-left: 10px;
|
||||
height: calc(100vh - 48px) !important;
|
||||
padding-left: 6px;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
|
||||
.emptyHolder {
|
||||
@@ -100,7 +100,7 @@
|
||||
.messageList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 20px 90px 4px;
|
||||
padding: 20px 20px 60px 4px;
|
||||
row-gap: 10px;
|
||||
|
||||
.messageItem {
|
||||
@@ -109,6 +109,16 @@
|
||||
row-gap: 10px;
|
||||
|
||||
:global {
|
||||
.ant-table-small {
|
||||
.ant-table-tbody {
|
||||
.ant-table-cell {
|
||||
padding: 6px 0 !important;
|
||||
}
|
||||
}
|
||||
.ss-chat-table-formatted-value {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
}
|
||||
.ant-table-row {
|
||||
background-color: #fff;
|
||||
}
|
||||
@@ -218,12 +228,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.miniProgramLoading {
|
||||
position: absolute;
|
||||
bottom: 10000px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +236,7 @@
|
||||
|
||||
&.conversationCollapsed {
|
||||
.chatApp {
|
||||
width: 100% !important;
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,19 +250,193 @@
|
||||
|
||||
.conversation {
|
||||
height: 100% !important;
|
||||
|
||||
.conversationList {
|
||||
height: calc(100% - 50px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chatApp {
|
||||
width: 100% !important;
|
||||
width: calc(100% - 260px) !important;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&.copilotFullscreen {
|
||||
.chatApp {
|
||||
width: calc(100% - 260px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.conversationCollapsed {
|
||||
.chatApp {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
height: calc(100vh - 48px) !important;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid var(--border-color-base);
|
||||
|
||||
.leftSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.searchConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 9px 10px;
|
||||
|
||||
.searchIcon {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.searchTask {
|
||||
font-size: 13px;
|
||||
background-color: #f5f5f5;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
|
||||
:global {
|
||||
.ant-input {
|
||||
font-size: 13px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
height: calc(calc(100vh - 48px) - 50px);
|
||||
padding: 2px 8px 0;
|
||||
overflow-y: auto;
|
||||
.conversationItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.conversationIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.conversationContent {
|
||||
width: 100%;
|
||||
|
||||
.topTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversationName {
|
||||
width: 150px;
|
||||
margin-right: 2px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conversationTime {
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&.activeConversationItem {
|
||||
background-color: var(--light-blue-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
margin-top: 20px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.operateItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
|
||||
.operateIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.operateLabel {
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.operateLabel {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
|
||||
.leftSection {
|
||||
.searchConversation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.copilotMode {
|
||||
&.collapsed {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.messageList {
|
||||
padding: 20px 12px 20px !important;
|
||||
padding: 20px 12px 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +504,6 @@
|
||||
|
||||
.example {
|
||||
margin-right: 4px;
|
||||
// margin-left: 16px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -409,7 +586,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
// width: 530px;
|
||||
|
||||
&.recentSearchBar {
|
||||
padding-top: 2px;
|
||||
@@ -607,4 +783,14 @@
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-chat-item-typing-bubble {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
ss-chat-metric-card-drill-down-dimensions {
|
||||
bottom: -38px !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ import { MsgDataType } from 'supersonic-chat-sdk';
|
||||
export enum MessageTypeEnum {
|
||||
TEXT = 'text', // 指标文本
|
||||
QUESTION = 'question',
|
||||
TAG = 'tag', // 标签
|
||||
SUGGESTION = 'suggestion', // 建议
|
||||
NO_PERMISSION = 'no_permission', // 无权限
|
||||
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
|
||||
INSTRUCTION = 'INSTRUCTION', // 插件
|
||||
SUGGESTION = 'SUGGESTION',
|
||||
RECOMMEND_QUESTIONS = 'RECOMMEND_QUESTIONS' // 推荐问题
|
||||
PLUGIN = 'PLUGIN', // 插件
|
||||
WEB_PAGE = 'WEB_PAGE', // 插件
|
||||
RECOMMEND_QUESTIONS = 'recommend_questions', // 推荐问题
|
||||
}
|
||||
|
||||
export type MessageItem = {
|
||||
@@ -17,8 +19,11 @@ export type MessageItem = {
|
||||
msgValue?: string;
|
||||
identityMsg?: string;
|
||||
domainId?: number;
|
||||
entityId?: string;
|
||||
msgData?: MsgDataType;
|
||||
quote?: string;
|
||||
score?: number;
|
||||
feedback?: string;
|
||||
isHistory?: boolean;
|
||||
};
|
||||
|
||||
@@ -31,6 +36,7 @@ export type ConversationDetailType = {
|
||||
lastTime?: string;
|
||||
initMsg?: string;
|
||||
domainId?: number;
|
||||
entityId?: string;
|
||||
};
|
||||
|
||||
export enum MessageModeEnum {
|
||||
@@ -44,6 +50,25 @@ export type DomainType = {
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export enum PluginShowTypeEnum {
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
WIDGET = 'WIDGET',
|
||||
URL = 'URL',
|
||||
TAG = 'TAG',
|
||||
}
|
||||
|
||||
export type PluginType = {
|
||||
id: number;
|
||||
name: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type DefaultEntityType = {
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
domainName?: string;
|
||||
};
|
||||
|
||||
export type SuggestionItemType = {
|
||||
id: number;
|
||||
domain: number;
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Select, Form, Input, InputNumber, message, Button, Radio } from 'antd';
|
||||
import { getDimensionList, getDomainList, savePlugin } from './service';
|
||||
import {
|
||||
DimensionType,
|
||||
DomainType,
|
||||
ParamTypeEnum,
|
||||
ParseModeEnum,
|
||||
PluginType,
|
||||
FunctionParamFormItemType,
|
||||
PluginTypeEnum,
|
||||
} from './type';
|
||||
import { getLeafList, uuid } from '@/utils/utils';
|
||||
import styles from './style.less';
|
||||
import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { isArray, set } from 'lodash';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { TextArea } = Input;
|
||||
|
||||
type Props = {
|
||||
detail?: PluginType;
|
||||
onSubmit: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
|
||||
const [domainList, setDomainList] = useState<DomainType[]>([]);
|
||||
const [domainDimensionList, setDomainDimensionList] = useState<Record<number, DimensionType[]>>(
|
||||
{},
|
||||
);
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [pluginType, setPluginType] = useState<PluginTypeEnum>();
|
||||
const [functionName, setFunctionName] = useState<string>();
|
||||
const [functionParams, setFunctionParams] = useState<FunctionParamFormItemType[]>([]);
|
||||
const [examples, setExamples] = useState<{ id: string; question?: string }[]>([]);
|
||||
const [filters, setFilters] = useState<any[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const initDomainList = async () => {
|
||||
const res = await getDomainList();
|
||||
setDomainList([{ id: -1, name: '全部' }, ...getLeafList(res.data)]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initDomainList();
|
||||
}, []);
|
||||
|
||||
const initDomainDimensions = async (params: any) => {
|
||||
const domainIds = params
|
||||
.filter((param: any) => !!param.domainId)
|
||||
.map((param: any) => param.domainId);
|
||||
const res = await Promise.all(domainIds.map((domainId: number) => getDimensionList(domainId)));
|
||||
setDomainDimensionList(
|
||||
domainIds.reduce(
|
||||
(result: Record<number, DimensionType[]>, domainId: number, index: number) => {
|
||||
result[domainId] = res[index].data.list;
|
||||
return result;
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (detail) {
|
||||
const { paramOptions } = detail.config || {};
|
||||
const height = paramOptions?.find(
|
||||
(option: any) => option.paramType === 'FORWARD' && option.key === 'height',
|
||||
)?.value;
|
||||
form.setFieldsValue({
|
||||
...detail,
|
||||
url: detail.config?.url,
|
||||
height,
|
||||
});
|
||||
if (paramOptions?.length > 0) {
|
||||
const params = paramOptions.filter(
|
||||
(option: any) => option.paramType !== ParamTypeEnum.FORWARD,
|
||||
);
|
||||
setFilters(params);
|
||||
initDomainDimensions(params);
|
||||
}
|
||||
setPluginType(detail.type);
|
||||
const parseModeObj = JSON.parse(detail.parseModeConfig || '{}');
|
||||
setFunctionName(parseModeObj.name);
|
||||
const { properties } = parseModeObj.parameters || {};
|
||||
setFunctionParams(
|
||||
properties
|
||||
? Object.keys(properties).map((key: string, index: number) => {
|
||||
return {
|
||||
id: `${index}`,
|
||||
name: key,
|
||||
type: properties[key].type,
|
||||
description: properties[key].description,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
);
|
||||
setExamples(
|
||||
parseModeObj.examples
|
||||
? parseModeObj.examples.map((item: string, index: number) => ({
|
||||
id: index,
|
||||
question: item,
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
}
|
||||
}, [detail]);
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 20 },
|
||||
};
|
||||
|
||||
const getFunctionParam = (description: string) => {
|
||||
return {
|
||||
name: functionName,
|
||||
description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: functionParams
|
||||
.filter((param) => !!param.name?.trim())
|
||||
.reduce((acc, cur) => {
|
||||
acc[cur.name || ''] = {
|
||||
type: cur.type,
|
||||
description: cur.description,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
required: functionParams.filter((param) => !!param.name?.trim()).map((param) => param.name),
|
||||
},
|
||||
examples: examples
|
||||
.filter((example) => !!example.question?.trim())
|
||||
.map((example) => example.question),
|
||||
};
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
setConfirmLoading(true);
|
||||
let paramOptions = isArray(filters)
|
||||
? filters?.filter(
|
||||
(filter) =>
|
||||
typeof filter === 'object' && (filter.paramType !== null || filter.value != null),
|
||||
)
|
||||
: [];
|
||||
paramOptions = paramOptions.concat([
|
||||
{
|
||||
paramType: ParamTypeEnum.FORWARD,
|
||||
key: 'height',
|
||||
value: values.height || undefined,
|
||||
},
|
||||
]);
|
||||
const config = {
|
||||
url: values.url,
|
||||
paramOptions,
|
||||
};
|
||||
await savePlugin({
|
||||
...values,
|
||||
id: detail?.id,
|
||||
domainList: isArray(values.domainList) ? values.domainList : [values.domainList],
|
||||
config: JSON.stringify(config),
|
||||
parseModeConfig: JSON.stringify(getFunctionParam(values.pattern)),
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
onSubmit(values);
|
||||
message.success(detail?.id ? '编辑成功' : '新建成功');
|
||||
};
|
||||
|
||||
const updateDimensionList = async (value: number) => {
|
||||
if (domainDimensionList[value]) {
|
||||
return;
|
||||
}
|
||||
const res = await getDimensionList(value);
|
||||
setDomainDimensionList({ ...domainDimensionList, [value]: res.data.list });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={detail ? '编辑插件' : '新建插件'}
|
||||
width={900}
|
||||
confirmLoading={confirmLoading}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form {...layout} form={form} style={{ maxWidth: 820 }}>
|
||||
<FormItem name="domainList" label="主题域">
|
||||
<Select
|
||||
placeholder="请选择主题域"
|
||||
options={domainList.map((domain) => ({
|
||||
label: domain.name,
|
||||
value: domain.id,
|
||||
}))}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
mode="multiple"
|
||||
allowClear
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="name"
|
||||
label="插件名称"
|
||||
rules={[{ required: true, message: '请输入插件名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入插件名称" allowClear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="type"
|
||||
label="插件类型"
|
||||
rules={[{ required: true, message: '请选择插件类型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择插件类型"
|
||||
options={Object.keys(PLUGIN_TYPE_MAP).map((key) => ({
|
||||
label: PLUGIN_TYPE_MAP[key],
|
||||
value: key,
|
||||
}))}
|
||||
onChange={(value) => {
|
||||
setPluginType(value);
|
||||
if (value === PluginTypeEnum.DSL) {
|
||||
form.setFieldsValue({ parseMode: ParseModeEnum.FUNCTION_CALL });
|
||||
// setFunctionName('DSL');
|
||||
setFunctionParams([
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'query_text',
|
||||
type: 'string',
|
||||
description: '用户的原始自然语言查询',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="pattern"
|
||||
label="插件描述"
|
||||
rules={[{ required: true, message: '请输入插件描述' }]}
|
||||
>
|
||||
<TextArea placeholder="请输入插件描述,多个描述换行分隔" allowClear />
|
||||
</FormItem>
|
||||
<FormItem name="exampleQuestions" label="示例问题">
|
||||
<div className={styles.paramsSection}>
|
||||
{examples.map((example) => {
|
||||
const { id, question } = example;
|
||||
return (
|
||||
<div className={styles.filterRow} key={id}>
|
||||
<Input
|
||||
placeholder="示例问题"
|
||||
value={question}
|
||||
className={styles.questionExample}
|
||||
onChange={(e) => {
|
||||
example.question = e.target.value;
|
||||
setExamples([...examples]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
setExamples(examples.filter((item) => item.id !== id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setExamples([...examples, { id: uuid() }]);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新增示例问题
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem label="函数名称">
|
||||
<Input
|
||||
value={functionName}
|
||||
onChange={(e) => {
|
||||
setFunctionName(e.target.value);
|
||||
}}
|
||||
placeholder="请输入函数名称,只能包含因为字母和下划线"
|
||||
allowClear
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="params" label="函数参数" hidden={pluginType === PluginTypeEnum.DSL}>
|
||||
<div className={styles.paramsSection}>
|
||||
{functionParams.map((functionParam: FunctionParamFormItemType) => {
|
||||
const { id, name, type, description } = functionParam;
|
||||
return (
|
||||
<div className={styles.filterRow} key={id}>
|
||||
<Input
|
||||
placeholder="参数名称"
|
||||
value={name}
|
||||
className={styles.filterParamName}
|
||||
onChange={(e) => {
|
||||
functionParam.name = e.target.value;
|
||||
setFunctionParams([...functionParams]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="参数类型"
|
||||
options={[
|
||||
{ label: '字符串', value: 'string' },
|
||||
{ label: '整型', value: 'int' },
|
||||
]}
|
||||
className={styles.filterParamValueField}
|
||||
allowClear
|
||||
value={type}
|
||||
onChange={(value) => {
|
||||
functionParam.type = value;
|
||||
setFunctionParams([...functionParams]);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
placeholder="参数描述"
|
||||
value={description}
|
||||
className={styles.filterParamValueField}
|
||||
onChange={(e) => {
|
||||
functionParam.description = e.target.value;
|
||||
setFunctionParams([...functionParams]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
setFunctionParams(functionParams.filter((item) => item.id !== id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFunctionParams([...functionParams, { id: uuid() }]);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新增函数参数
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
{(pluginType === PluginTypeEnum.WEB_PAGE || pluginType === PluginTypeEnum.WEB_SERVICE) && (
|
||||
<>
|
||||
<FormItem name="url" label="地址" rules={[{ required: true, message: '请输入地址' }]}>
|
||||
<Input placeholder="请输入地址" allowClear />
|
||||
</FormItem>
|
||||
<FormItem name="params" label="参数">
|
||||
<div className={styles.paramsSection}>
|
||||
{filters.map((filter: any) => {
|
||||
return (
|
||||
<div className={styles.filterRow} key={filter.id}>
|
||||
<Input
|
||||
placeholder="参数名称"
|
||||
value={filter.key}
|
||||
className={styles.filterParamName}
|
||||
onChange={(e) => {
|
||||
filter.key = e.target.value;
|
||||
setFilters([...filters]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<Radio.Group
|
||||
onChange={(e) => {
|
||||
filter.paramType = e.target.value;
|
||||
setFilters([...filters]);
|
||||
}}
|
||||
value={filter.paramType}
|
||||
>
|
||||
<Radio value={ParamTypeEnum.SEMANTIC}>维度</Radio>
|
||||
<Radio value={ParamTypeEnum.CUSTOM}>自定义</Radio>
|
||||
</Radio.Group>
|
||||
{filter.paramType === ParamTypeEnum.CUSTOM && (
|
||||
<Input
|
||||
placeholder="请输入"
|
||||
value={filter.value}
|
||||
className={styles.filterParamValueField}
|
||||
onChange={(e) => {
|
||||
filter.value = e.target.value;
|
||||
setFilters([...filters]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
)}
|
||||
{filter.paramType === ParamTypeEnum.SEMANTIC && (
|
||||
<>
|
||||
<Select
|
||||
placeholder="主题域"
|
||||
options={domainList.map((domain) => ({
|
||||
label: domain.name,
|
||||
value: domain.id,
|
||||
}))}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string)
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
className={styles.filterParamName}
|
||||
allowClear
|
||||
value={filter.domainId}
|
||||
onChange={(value) => {
|
||||
filter.domainId = value;
|
||||
setFilters([...filters]);
|
||||
updateDimensionList(value);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder="请选择维度,需先选择主题域"
|
||||
options={(domainDimensionList[filter.domainId] || []).map(
|
||||
(dimension) => ({
|
||||
label: dimension.name,
|
||||
value: `${dimension.id}`,
|
||||
}),
|
||||
)}
|
||||
showSearch
|
||||
className={styles.filterParamValueField}
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string)
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
allowClear
|
||||
value={filter.elementId}
|
||||
onChange={(value) => {
|
||||
filter.elementId = value;
|
||||
setFilters([...filters]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
setFilters(filters.filter((item) => item.id !== filter.id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFilters([...filters, { id: uuid(), key: undefined, value: undefined }]);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新增参数
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
<FormItem name="height" label="高度">
|
||||
<InputNumber placeholder="单位px" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailModal;
|
||||
@@ -0,0 +1,17 @@
|
||||
export const PLUGIN_TYPE_MAP = {
|
||||
WEB_PAGE: '外链页面',
|
||||
WEB_SERVICE: 'Web服务',
|
||||
DSL: 'LLM语义解析',
|
||||
}
|
||||
|
||||
export const PARSE_MODE_MAP = {
|
||||
EMBEDDING_RECALL: '向量召回',
|
||||
FUNCTION_CALL: '函数调用'
|
||||
}
|
||||
|
||||
export const PLUGIN_COLOR_MAP = {
|
||||
WIDGET: 'blue',
|
||||
DASHBOARD: 'volcano',
|
||||
URL: 'purple',
|
||||
TAG: 'cyan',
|
||||
}
|
||||
248
webapp/packages/supersonic-fe/src/pages/ChatPlugin/index.tsx
Normal file
248
webapp/packages/supersonic-fe/src/pages/ChatPlugin/index.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { getLeafList } from '@/utils/utils';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, message, Popconfirm, Select, Table, Tag } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants';
|
||||
import DetailModal from './DetailModal';
|
||||
import { deletePlugin, getDomainList, getPluginList } from './service';
|
||||
import styles from './style.less';
|
||||
import { DomainType, ParseModeEnum, PluginType, PluginTypeEnum } from './type';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
const PluginManage = () => {
|
||||
const [name, setName] = useState<string>();
|
||||
const [type, setType] = useState<PluginTypeEnum>();
|
||||
const [pattern, setPattern] = useState<string>();
|
||||
const [domain, setDomain] = useState<string>();
|
||||
const [data, setData] = useState<PluginType[]>([]);
|
||||
const [domainList, setDomainList] = useState<DomainType[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginType>();
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
|
||||
const initDomainList = async () => {
|
||||
const res = await getDomainList();
|
||||
setDomainList(getLeafList(res.data));
|
||||
};
|
||||
|
||||
const updateData = async (filters?: any) => {
|
||||
setLoading(true);
|
||||
const res = await getPluginList({ name, type, pattern, domain, ...(filters || {}) });
|
||||
setLoading(false);
|
||||
setData(res.data.map((item) => ({ ...item, config: JSON.parse(item.config || '{}') })));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initDomainList();
|
||||
updateData();
|
||||
}, []);
|
||||
|
||||
const onCheckPluginDetail = (record: PluginType) => {
|
||||
setCurrentPluginDetail(record);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const onDeletePlugin = async (record: PluginType) => {
|
||||
await deletePlugin(record.id);
|
||||
message.success('插件删除成功');
|
||||
updateData();
|
||||
};
|
||||
|
||||
const columns: any[] = [
|
||||
{
|
||||
title: '插件名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '主题域',
|
||||
dataIndex: 'domainList',
|
||||
key: 'domainList',
|
||||
width: 200,
|
||||
render: (value: number[]) => {
|
||||
if (value?.includes(-1)) {
|
||||
return '全部';
|
||||
}
|
||||
return value ? (
|
||||
<div className={styles.domainColumn}>
|
||||
{value.map((id, index) => {
|
||||
const name = domainList.find((domain) => domain.id === +id)?.name;
|
||||
return name ? <Tag key={id}>{name}</Tag> : null;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '插件类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (value: string) => {
|
||||
return (
|
||||
<Tag color={value === PluginTypeEnum.WEB_PAGE ? 'blue' : 'cyan'}>
|
||||
{PLUGIN_TYPE_MAP[value]}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '插件描述',
|
||||
dataIndex: 'pattern',
|
||||
key: 'pattern',
|
||||
width: 450,
|
||||
},
|
||||
{
|
||||
title: '更新人',
|
||||
dataIndex: 'updatedBy',
|
||||
key: 'updatedBy',
|
||||
render: (value: string) => {
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updatedAt',
|
||||
key: 'updatedAt',
|
||||
render: (value: string) => {
|
||||
return value ? moment(value).format('YYYY-MM-DD HH:mm') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'x',
|
||||
key: 'x',
|
||||
render: (_: any, record: any) => {
|
||||
return (
|
||||
<div className={styles.operator}>
|
||||
<a
|
||||
onClick={() => {
|
||||
onCheckPluginDetail(record);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
<Popconfirm
|
||||
title="确定删除吗?"
|
||||
onConfirm={() => {
|
||||
onDeletePlugin(record);
|
||||
}}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const onDomainChange = (value: string) => {
|
||||
setDomain(value);
|
||||
updateData({ domain: value });
|
||||
};
|
||||
|
||||
const onTypeChange = (value: PluginTypeEnum) => {
|
||||
setType(value);
|
||||
updateData({ type: value });
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
updateData();
|
||||
};
|
||||
|
||||
const onCreatePlugin = () => {
|
||||
setCurrentPluginDetail(undefined);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const onSavePlugin = () => {
|
||||
setDetailModalVisible(false);
|
||||
updateData();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.pluginManage}>
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterItem}>
|
||||
<div className={styles.filterItemTitle}>主题域</div>
|
||||
<Select
|
||||
className={styles.filterItemControl}
|
||||
placeholder="请选择主题域"
|
||||
options={domainList.map((domain) => ({ label: domain.name, value: domain.id }))}
|
||||
value={domain}
|
||||
allowClear
|
||||
onChange={onDomainChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterItem}>
|
||||
<div className={styles.filterItemTitle}>插件名称</div>
|
||||
<Search
|
||||
className={styles.filterItemControl}
|
||||
placeholder="请输入插件名称"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterItem}>
|
||||
<div className={styles.filterItemTitle}>插件描述</div>
|
||||
<Search
|
||||
className={styles.filterItemControl}
|
||||
placeholder="请输入插件描述"
|
||||
value={pattern}
|
||||
onChange={(e) => {
|
||||
setPattern(e.target.value);
|
||||
}}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterItem}>
|
||||
<div className={styles.filterItemTitle}>插件类型</div>
|
||||
<Select
|
||||
className={styles.filterItemControl}
|
||||
placeholder="请选择插件类型"
|
||||
options={Object.keys(PLUGIN_TYPE_MAP).map((key) => ({
|
||||
label: PLUGIN_TYPE_MAP[key],
|
||||
value: key,
|
||||
}))}
|
||||
value={type}
|
||||
allowClear
|
||||
onChange={onTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.pluginList}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={styles.title}>插件列表</div>
|
||||
<Button type="primary" onClick={onCreatePlugin}>
|
||||
<PlusOutlined />
|
||||
新建插件
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
size="small"
|
||||
pagination={{ defaultPageSize: 20 }}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
{detailModalVisible && (
|
||||
<DetailModal
|
||||
detail={currentPluginDetail}
|
||||
onSubmit={onSavePlugin}
|
||||
onCancel={() => {
|
||||
setDetailModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginManage;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { request } from "umi";
|
||||
import { DimensionType, DomainType, PluginType } from "./type";
|
||||
|
||||
export function savePlugin(params: Partial<PluginType>) {
|
||||
return request<Result<any>>('/api/chat/plugin', {
|
||||
method: params.id ? 'PUT' : 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export function getPluginList(filters?: any) {
|
||||
return request<Result<any[]>>('/api/chat/plugin/query', {
|
||||
method: 'POST',
|
||||
data: filters
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePlugin(id: number) {
|
||||
return request<Result<any>>(`/api/chat/plugin/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export function getDomainList() {
|
||||
return request<Result<DomainType[]>>('/api/chat/conf/domainList', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function getDimensionList(domainId: number) {
|
||||
return request<Result<{list: DimensionType[]}>>('/api/semantic/dimension/queryDimension', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
domainIds: [domainId],
|
||||
current: 1,
|
||||
pageSize: 2000
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
.pluginManage {
|
||||
.filterSection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 12px;
|
||||
column-gap: 30px;
|
||||
margin: 12px 24px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
.filterItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
width: 22vw;
|
||||
|
||||
.filterItemTitle {
|
||||
width: 60px;
|
||||
margin-right: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filterItemControl {
|
||||
// width: 20vw;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pluginList {
|
||||
margin: 12px 24px;
|
||||
padding: 12px 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
.titleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.domainColumn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
.paramsSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
.filterRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
|
||||
.filterParamName {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.filterParamValueField {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.questionExample {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
webapp/packages/supersonic-fe/src/pages/ChatPlugin/type.ts
Normal file
68
webapp/packages/supersonic-fe/src/pages/ChatPlugin/type.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export type PluginConfigType = {
|
||||
url: string;
|
||||
params: any;
|
||||
paramOptions: any;
|
||||
valueParams: any;
|
||||
forwardParam: any;
|
||||
}
|
||||
|
||||
export enum PluginTypeEnum {
|
||||
WEB_PAGE = 'WEB_PAGE',
|
||||
WEB_SERVICE = 'WEB_SERVICE',
|
||||
DSL = 'DSL'
|
||||
}
|
||||
|
||||
export enum ParseModeEnum {
|
||||
EMBEDDING_RECALL = 'EMBEDDING_RECALL',
|
||||
FUNCTION_CALL = 'FUNCTION_CALL'
|
||||
}
|
||||
|
||||
export enum ParamTypeEnum {
|
||||
CUSTOM = 'CUSTOM',
|
||||
SEMANTIC = 'SEMANTIC',
|
||||
FORWARD = 'FORWARD'
|
||||
}
|
||||
|
||||
export type PluginType = {
|
||||
id: number;
|
||||
type: PluginTypeEnum;
|
||||
domainList: number[];
|
||||
pattern: string;
|
||||
parseMode: ParseModeEnum;
|
||||
parseModeConfig: string;
|
||||
name: string;
|
||||
config: PluginConfigType;
|
||||
}
|
||||
|
||||
export type DomainType = {
|
||||
id: number | string;
|
||||
parentId: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export type DimensionType = {
|
||||
id: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export type FunctionParamType = {
|
||||
type: string;
|
||||
properties: Record<string, { type: string, description: string }>;
|
||||
required: string[];
|
||||
}
|
||||
|
||||
export type FunctionType = {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: FunctionParamType;
|
||||
examples: string[];
|
||||
}
|
||||
|
||||
export type FunctionParamFormItemType = {
|
||||
id: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
}
|
||||
@@ -422,3 +422,7 @@ export function traverseRoutes(routes, env: string, result: any[] = []) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isProd() {
|
||||
return process.env.NODE_ENV === 'production';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user