[feature](webapp) merge query steps to one card

This commit is contained in:
williamhliu
2023-08-29 22:14:14 +08:00
parent 93ca060c45
commit 36fd737440
40 changed files with 994 additions and 496 deletions

View File

@@ -1,13 +1,14 @@
import { Spin } from 'antd';
import { CheckCircleFilled } from '@ant-design/icons';
import { PREFIX_CLS } from '../../common/constants';
import { MsgDataType } from '../../common/type';
import ChatMsg from '../ChatMsg';
import Tools from '../Tools';
import Text from './Text';
import Typing from './Typing';
import WebPage from '../ChatMsg/WebPage';
import Loading from './Loading';
type Props = {
question: string;
queryId?: number;
executeLoading: boolean;
entitySwitchLoading: boolean;
chartIndex: number;
@@ -15,13 +16,12 @@ type Props = {
data?: MsgDataType;
isMobileMode?: boolean;
triggerResize?: boolean;
isLastMessage?: boolean;
onSwitchEntity: (entityId: string) => void;
onChangeChart: () => void;
};
const ExecuteItem: React.FC<Props> = ({
question,
queryId,
executeLoading,
entitySwitchLoading,
chartIndex,
@@ -29,49 +29,59 @@ const ExecuteItem: React.FC<Props> = ({
data,
isMobileMode,
triggerResize,
isLastMessage,
onSwitchEntity,
onChangeChart,
}) => {
const prefixCls = `${PREFIX_CLS}-item`;
const getNodeTip = (title: string, tip?: string) => {
return (
<>
<div className={`${prefixCls}-title-bar`}>
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
<div className={`${prefixCls}-step-title`}>
{title}
{!tip && <Loading />}
</div>
</div>
{tip && <div className={`${prefixCls}-content-container`}>{tip}</div>}
</>
);
};
if (executeLoading) {
return <Typing />;
return getNodeTip('数据查询中');
}
if (executeTip) {
return <Text data={executeTip} />;
return getNodeTip('数据查询失败', executeTip);
}
if (!data || data.queryMode === 'WEB_PAGE') {
if (!data) {
return null;
}
const isMetricCard =
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
data.queryResults?.length === 1;
return (
<div className={`${prefixCls}-msg-content`}>
<Spin spinning={entitySwitchLoading}>
<ChatMsg
question={question}
data={data}
chartIndex={chartIndex}
isMobileMode={isMobileMode}
triggerResize={triggerResize}
/>
</Spin>
{!isMetricCard && (
<Tools
data={data}
isLastMessage={isLastMessage}
isMobileMode={isMobileMode}
onSwitchEntity={onSwitchEntity}
onChangeChart={onChangeChart}
/>
)}
</div>
<>
<div className={`${prefixCls}-title-bar`}>
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
<div className={`${prefixCls}-step-title`}></div>
</div>
<div className={`${prefixCls}-content-container ${prefixCls}-last-node`}>
<Spin spinning={entitySwitchLoading}>
{data?.queryMode === 'WEB_PAGE' ? (
<WebPage id={queryId!} data={data} />
) : (
<ChatMsg
question={question}
data={data}
chartIndex={chartIndex}
isMobileMode={isMobileMode}
triggerResize={triggerResize}
/>
)}
</Spin>
</div>
</>
);
};

View File

@@ -0,0 +1,14 @@
import { PREFIX_CLS } from '../../common/constants';
const Loading = () => {
const prefixCls = `${PREFIX_CLS}-item`;
return (
<span className={`${prefixCls}-loading`}>
<span className={`${prefixCls}-loading-dot`} />
<span className={`${prefixCls}-loading-dot`} />
<span className={`${prefixCls}-loading-dot`} />
</span>
);
};
export default Loading;

View File

@@ -1,9 +1,11 @@
import React, { ReactNode } from 'react';
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
import { ChatContextType } from '../../common/type';
import Text from './Text';
import Typing from './Typing';
import { CheckCircleFilled, InfoCircleOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import SwicthEntity from './SwitchEntity';
import { Tooltip } from 'antd';
import Loading from './Loading';
type Props = {
parseLoading: boolean;
@@ -12,6 +14,7 @@ type Props = {
currentParseInfo?: ChatContextType;
optionMode?: boolean;
onSelectParseInfo: (parseInfo: ChatContextType) => void;
onSwitchEntity: (entityId: string) => void;
};
const MAX_OPTION_VALUES_COUNT = 2;
@@ -23,15 +26,34 @@ const ParseTip: React.FC<Props> = ({
currentParseInfo,
optionMode,
onSelectParseInfo,
onSwitchEntity,
}) => {
const prefixCls = `${PREFIX_CLS}-item`;
const getNode = (tipTitle: string, tipNode?: ReactNode, parseSucceed?: boolean) => {
const contentContainerClass = classNames(`${prefixCls}-content-container`, {
[`${prefixCls}-content-container-succeed`]: parseSucceed,
});
return (
<div className={`${prefixCls}-parse-tip`}>
<div className={`${prefixCls}-title-bar`}>
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
<div className={`${prefixCls}-step-title`}>
{tipTitle}
{!tipNode && <Loading />}
</div>
</div>
{tipNode && <div className={contentContainerClass}>{tipNode}</div>}
</div>
);
};
if (parseLoading) {
return <Typing />;
return getNode('意图解析中');
}
if (parseTip) {
return <Text data={parseTip} />;
return getNode('意图解析失败', parseTip);
}
if (parseInfoOptions.length === 0) {
@@ -81,6 +103,45 @@ const ParseTip: React.FC<Props> = ({
const fields =
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
const getFilterContent = (filters: any) => {
return (
<div className={`${prefixCls}-tip-item-filter-content`}>
{filters.map((filter: any, index: number) => (
<div className={itemValueClass}>
<span>
{filter.name}
{filter.operator !== '=' ? ` ${filter.operator} ` : ''}
</span>
<span>{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}</span>
{index !== filters.length - 1 && <span></span>}
</div>
))}
</div>
);
};
const getFiltersNode = () => {
return (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div>
<Tooltip
title={
dimensionFilters.length > MAX_OPTION_VALUES_COUNT
? getFilterContent(dimensionFilters)
: ''
}
color="#fff"
overlayStyle={{ maxWidth: 'none' }}
>
<div className={`${prefixCls}-tip-item-content`}>
{getFilterContent(dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT))}
{dimensionFilters.length > MAX_OPTION_VALUES_COUNT && ' ...'}
</div>
</Tooltip>
</div>
);
};
return (
<div
className={tipContentClass}
@@ -91,20 +152,28 @@ const ParseTip: React.FC<Props> = ({
}}
>
{index !== undefined && <div>{index + 1}.</div>}
{!!agentType ? (
{!!agentType && queryMode !== 'DSL' ? (
<div className={`${prefixCls}-tip-item`}>
{agentType === 'plugin' ? '插件' : '内置'}
<span className={itemValueClass}>{agentName}</span>
</div>
) : (
<>
{queryMode.includes('ENTITY') &&
{(queryMode.includes('ENTITY') || queryMode === 'DSL') &&
typeof entityId === 'string' &&
!!entityAlias &&
!!entityName ? (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}</div>
<div className={itemValueClass}>{entityName}</div>
{!isOptions && (entityAlias === '歌曲' || entityAlias === '艺人') ? (
<SwicthEntity
entityName={entityName}
chatContext={parseInfo}
onSwitchEntity={onSwitchEntity}
/>
) : (
<div className={itemValueClass}>{entityName}</div>
)}
</div>
) : (
<div className={`${prefixCls}-tip-item`}>
@@ -112,7 +181,7 @@ const ParseTip: React.FC<Props> = ({
<div className={itemValueClass}>{modelName}</div>
</div>
)}
{metric && (
{queryMode !== 'ENTITY_ID' && metric && (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div>
<div className={itemValueClass}>{metric.name}</div>
@@ -120,7 +189,7 @@ const ParseTip: React.FC<Props> = ({
)}
{!isOptions && (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div>
<div className={`${prefixCls}-tip-item-name`}></div>
<div className={itemValueClass}>
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
</div>
@@ -148,23 +217,11 @@ const ParseTip: React.FC<Props> = ({
'ENTITY_DETAIL',
'ENTITY_LIST_FILTER',
'ENTITY_ID',
'DSL',
].includes(queryMode) &&
dimensionFilters &&
dimensionFilters?.length > 0 && (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div>
{dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT).map((filter, index) => (
<div className={itemValueClass}>
<span>{filter.name}</span>
<span>
{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}
</span>
{index !== dimensionFilters.length - 1 && <span></span>}
</div>
))}
{dimensionFilters.length > MAX_OPTION_VALUES_COUNT && '...'}
</div>
)}
dimensionFilters?.length > 0 &&
getFiltersNode()}
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div>
@@ -191,16 +248,29 @@ const ParseTip: React.FC<Props> = ({
</div>
);
} else {
const agentType = parseInfoOptions[0]?.properties?.type;
const { type } = parseInfoOptions[0]?.properties || {};
const entityAlias = parseInfoOptions[0]?.entity?.alias?.[0]?.split('.')?.[0];
const entityName = parseInfoOptions[0]?.elementMatches?.find(
item => item.element?.type === 'ID'
)?.element.name;
const queryMode = parseInfoOptions[0]?.queryMode;
tipNode = (
<div className={`${prefixCls}-tip`}>
<div>{!!agentType ? '您的问题' : '您的问题解析为:'}</div>
{getTipNode(parseInfoOptions[0])}
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
<div className={`${prefixCls}-switch-entity-tip`}>
<InfoCircleOutlined />
<div>
{entityAlias}{entityAlias}
</div>
</div>
)}
</div>
);
}
return <Text data={tipNode} />;
return getNode('意图解析结果', tipNode, true);
};
export default ParseTip;

View File

@@ -0,0 +1,52 @@
import { useState } from 'react';
import { ChatContextType } from '../../common/type';
import { Popover } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import RecommendOptions from '../RecommendOptions';
import { PREFIX_CLS } from '../../common/constants';
type Props = {
entityName: string;
chatContext: ChatContextType;
onSwitchEntity: (entityId: string) => void;
};
const SwicthEntity: React.FC<Props> = ({ entityName, chatContext, onSwitchEntity }) => {
const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false);
const { modelId, modelName, dimensionFilters } = chatContext || {};
const prefixCls = `${PREFIX_CLS}-item`;
const switchEntity = (option: string) => {
setRecommendOptionsOpen(false);
onSwitchEntity(option);
};
const entityId = dimensionFilters?.find(
filter => filter?.bizName === 'zyqk_song_id' || filter?.bizName === 'singer_id'
)?.value;
return (
<Popover
content={
<RecommendOptions
entityId={entityId!}
modelId={modelId}
modelName={modelName}
onSelect={switchEntity}
/>
}
placement="bottomLeft"
trigger="click"
open={recommendOptionsOpen}
onOpenChange={open => setRecommendOptionsOpen(open)}
>
<div className={`${prefixCls}-tip-item-value ${prefixCls}-switch-entity`}>
{entityName}
<DownOutlined className={`${prefixCls}-down-icon`} />
</div>
</Popover>
);
};
export default SwicthEntity;

View File

@@ -7,6 +7,7 @@ import ParseTip from './ParseTip';
import ExecuteItem from './ExecuteItem';
import { isMobile } from '../../utils/utils';
import classNames from 'classnames';
import Tools from '../Tools';
type Props = {
msg: string;
@@ -17,6 +18,7 @@ type Props = {
isLastMessage?: boolean;
msgData?: MsgDataType;
isMobileMode?: boolean;
isHistory?: boolean;
triggerResize?: boolean;
parseOptions?: ChatContextType[];
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
@@ -31,6 +33,7 @@ const ChatItem: React.FC<Props> = ({
filter,
isLastMessage,
isMobileMode,
isHistory,
triggerResize,
msgData,
parseOptions,
@@ -118,15 +121,12 @@ const ChatItem: React.FC<Props> = ({
const { data: parseData } = await chatParse(msg, conversationId, modelId, agentId, filter);
setParseLoading(false);
const { code, data } = parseData || {};
const { state, selectedParses } = data || {};
const { state, selectedParses, queryId } = data || {};
if (
code !== 200 ||
state === ParseStateEnum.FAILED ||
selectedParses == null ||
selectedParses.length === 0 ||
(selectedParses.length > 0 &&
!selectedParses[0]?.properties?.type &&
!selectedParses[0]?.queryMode)
!selectedParses?.length ||
(!selectedParses[0]?.properties?.type && !selectedParses[0]?.queryMode)
) {
setParseTip(PARSE_ERROR_TIP);
return;
@@ -134,10 +134,14 @@ const ChatItem: React.FC<Props> = ({
if (onUpdateMessageScroll) {
onUpdateMessageScroll();
}
setParseInfoOptions(selectedParses || []);
const parseInfoValue = selectedParses[0];
const parseInfos = selectedParses.map(item => ({
...item,
queryId,
}));
setParseInfoOptions(parseInfos || []);
const parseInfoValue = parseInfos[0];
setParseInfo(parseInfoValue);
onExecute(parseInfoValue, selectedParses);
onExecute(parseInfoValue, parseInfos);
};
useEffect(() => {
@@ -158,6 +162,9 @@ const ChatItem: React.FC<Props> = ({
const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
setEntitySwitchLoading(false);
setData(res.data.data);
const { chatContext } = res.data.data;
setParseInfo(chatContext);
setParseInfoOptions([chatContext]);
};
const onChangeChart = () => {
@@ -176,10 +183,14 @@ const ChatItem: React.FC<Props> = ({
[`${prefixCls}-content-mobile`]: isMobile,
});
const isMetricCard =
(data?.queryMode === 'METRIC_DOMAIN' || data?.queryMode === 'METRIC_FILTER') &&
data?.queryResults?.length === 1;
return (
<div className={prefixCls}>
<div className={`${prefixCls}-section`}>
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
<div className={isMobile ? `${prefixCls}-mobile-msg-card` : `${prefixCls}-msg-card`}>
<div className={contentClass}>
<ParseTip
parseLoading={parseLoading}
@@ -188,29 +199,27 @@ const ChatItem: React.FC<Props> = ({
currentParseInfo={parseInfo}
optionMode={parseOptions !== undefined}
onSelectParseInfo={onSelectParseInfo}
onSwitchEntity={onSwitchEntity}
/>
</div>
</div>
{executeMode && data?.queryMode !== 'WEB_PAGE' && (
<div className={`${prefixCls}-section`}>
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
<div className={contentClass}>
{executeMode && (
<ExecuteItem
question={msg}
queryId={parseInfo?.queryId}
executeLoading={executeLoading}
entitySwitchLoading={entitySwitchLoading}
executeTip={executeTip}
chartIndex={chartIndex}
data={data}
isMobileMode={isMobileMode}
isLastMessage={isLastMessage}
triggerResize={triggerResize}
onSwitchEntity={onSwitchEntity}
onChangeChart={onChangeChart}
/>
</div>
)}
</div>
)}
{!isMetricCard && data && (
<Tools data={data} scoreValue={undefined} isLastMessage={isLastMessage} />
)}
</div>
</div>
);
};

View File

@@ -4,23 +4,144 @@
.@{chat-item-prefix-cls} {
display: flex;
flex-direction: column;
row-gap: 20px;
width: 100%;
&-section {
width: 100%;
&-loading {
display: inline-block;
width: 60px;
height: 20px;
}
&-loading-dot {
display: inline-block;
width: 4px;
height: 4px;
// border-radius: 50%;
background-color: var(--text-color);
margin: 0 2px;
opacity: 0;
animation: dot 1s ease-in-out infinite;
}
&-loading-dot:nth-child(1) {
animation-delay: 0s;
}
&-loading-dot:nth-child(2) {
animation-delay: 0.2s;
}
&-loading-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes dot {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.5);
}
}
&-avatar {
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
width: 40px;
height: 40px;
margin-right: 6px;
border-radius: 50%;
color: var(--chat-blue);
background-color: #fff;
}
&-mobile-msg-card {
width: 100%;
}
&-msg-card {
flex: 1;
}
&-content {
position: relative;
box-sizing: border-box;
min-width: 1px;
max-width: 100%;
padding: 12px 16px;
background: #fff;
border: 1px solid transparent;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
}
&-content-mobile {
width: 100%;
}
&-content-text {
margin-top: 12px;
}
&-msg-content {
width: 100%;
&-title-bar {
display: flex;
flex-direction: column;
align-items: center;
column-gap: 10px;
}
&-step-icon {
color: var(--green);
font-size: 16px;
}
&-content-container {
margin: 2px 0 2px 7px;
padding: 6px 0 4px 18px;
}
&-content-container-succeed {
border-left: 1px solid var(--green);
padding-bottom: 10px;
}
&-switch-entity-tip {
display: flex;
align-items: center;
column-gap: 6px;
margin-top: 4px;
color: var(--text-color-third);
font-size: 13px;
}
&-switch-entity {
cursor: pointer;
}
&-down-icon {
margin-left: 4px;
color: var(--text-color-fourth);
font-size: 12px;
}
&-last-node {
border-left: none;
margin-left: 0;
padding-left: 0;
}
&-chart-content {
padding: 6px 14px 12px;
border: 1px solid var(--border-color-base);
border-radius: 4px;
background: #f5f8fb;
}
&-multi-options {
@@ -39,10 +160,10 @@
&-tip {
display: flex;
align-items: center;
flex-direction: column;
row-gap: 6px;
flex-wrap: wrap;
color: var(--text-color);
color: var(--text-color-third);
}
&-tip-content {
@@ -51,7 +172,7 @@
flex-wrap: wrap;
row-gap: 6px;
column-gap: 12px;
color: var(--text-color);
color: var(--text-color-third);
}
&-tip-content-option {
@@ -86,6 +207,16 @@
align-items: center;
}
&-tip-item-content {
display: flex;
align-items: center;
}
&-tip-item-filter-content {
display: flex;
align-items: center;
}
&-mode-name {
margin-right: -10px;
font-weight: 500;
@@ -99,27 +230,6 @@
&-tip-item-option {
font-weight: 500;
}
&-avatar {
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
width: 40px;
height: 40px;
margin-right: 6px;
border-radius: 50%;
color: var(--chat-blue);
background-color: #fff;
}
&-content {
width: calc(100% - 50px);
}
&-content-mobile {
width: 100%;
}
&-metric-info-list {
margin-top: 30px;