mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-15 14:36:47 +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:
@@ -0,0 +1,73 @@
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
executeLoading: boolean;
|
||||
chartIndex: number;
|
||||
executeTip?: string;
|
||||
data?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
isLastMessage?: boolean;
|
||||
onSwitchEntity: (entityId: string) => void;
|
||||
onChangeChart: () => void;
|
||||
};
|
||||
|
||||
const ExecuteItem: React.FC<Props> = ({
|
||||
question,
|
||||
executeLoading,
|
||||
chartIndex,
|
||||
executeTip,
|
||||
data,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
isLastMessage,
|
||||
onSwitchEntity,
|
||||
onChangeChart,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
if (executeLoading) {
|
||||
return <Typing />;
|
||||
}
|
||||
|
||||
if (executeTip) {
|
||||
return <Text data={executeTip} />;
|
||||
}
|
||||
|
||||
if (!data || data.queryMode === 'WEB_PAGE') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMetricCard =
|
||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||
data.queryResults?.length === 1;
|
||||
|
||||
return (
|
||||
<div className={`${prefixCls}-msg-content`}>
|
||||
<ChatMsg
|
||||
question={question}
|
||||
data={data}
|
||||
chartIndex={chartIndex}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
{!isMetricCard && (
|
||||
<Tools
|
||||
data={data}
|
||||
isLastMessage={isLastMessage}
|
||||
isMobileMode={isMobileMode}
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onChangeChart={onChangeChart}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecuteItem;
|
||||
205
webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx
Normal file
205
webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
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 classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
parseLoading: boolean;
|
||||
parseInfoOptions: ChatContextType[];
|
||||
parseTip: string;
|
||||
currentParseInfo?: ChatContextType;
|
||||
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
||||
};
|
||||
|
||||
const MAX_OPTION_VALUES_COUNT = 2;
|
||||
|
||||
const ParseTip: React.FC<Props> = ({
|
||||
parseLoading,
|
||||
parseInfoOptions,
|
||||
parseTip,
|
||||
currentParseInfo,
|
||||
onSelectParseInfo,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
if (parseLoading) {
|
||||
return <Typing />;
|
||||
}
|
||||
|
||||
if (parseTip) {
|
||||
return <Text data={parseTip} />;
|
||||
}
|
||||
|
||||
if (parseInfoOptions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||
const {
|
||||
domainName,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
dimensions,
|
||||
metrics,
|
||||
aggType,
|
||||
queryMode,
|
||||
properties,
|
||||
entity,
|
||||
elementMatches,
|
||||
} = parseInfo || {};
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
const dimensionItems = dimensions?.filter(item => item.type === 'DIMENSION');
|
||||
const metric = metrics?.[0];
|
||||
|
||||
const tipContentClass = classNames(`${prefixCls}-tip-content`, {
|
||||
[`${prefixCls}-tip-content-option`]: isOptions,
|
||||
[`${prefixCls}-tip-content-option-active`]:
|
||||
isOptions &&
|
||||
currentParseInfo &&
|
||||
JSON.stringify(currentParseInfo) === JSON.stringify(parseInfo),
|
||||
[`${prefixCls}-tip-content-option-disabled`]:
|
||||
isOptions &&
|
||||
currentParseInfo !== undefined &&
|
||||
JSON.stringify(currentParseInfo) !== JSON.stringify(parseInfo),
|
||||
});
|
||||
|
||||
const itemValueClass = classNames({
|
||||
[`${prefixCls}-tip-item-value`]: !isOptions,
|
||||
[`${prefixCls}-tip-item-option`]: isOptions,
|
||||
});
|
||||
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||
|
||||
const pluginName = properties?.CONTEXT?.plugin?.name;
|
||||
|
||||
const modeName = pluginName
|
||||
? '调插件'
|
||||
: queryMode.includes('METRIC')
|
||||
? '算指标'
|
||||
: queryMode === 'ENTITY_DETAIL'
|
||||
? '查明细'
|
||||
: queryMode === 'ENTITY_LIST_FILTER'
|
||||
? '做圈选'
|
||||
: '';
|
||||
|
||||
const fields =
|
||||
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tipContentClass}
|
||||
onClick={() => {
|
||||
if (isOptions && currentParseInfo === undefined) {
|
||||
onSelectParseInfo(parseInfo);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{index !== undefined && <div>{index + 1}.</div>}
|
||||
{!pluginName && isOptions && <div className={`${prefixCls}-mode-name`}>{modeName}:</div>}
|
||||
{!!pluginName ? (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
将由问答插件
|
||||
<span className={itemValueClass}>{pluginName}</span>来解答
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL' ? (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}:</div>
|
||||
<div className={itemValueClass}>{entityName}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>主题域:</div>
|
||||
<div className={itemValueClass}>{domainName}</div>
|
||||
</div>
|
||||
)}
|
||||
{modeName === '算指标' && metric && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
||||
<div className={itemValueClass}>{metric.name}</div>
|
||||
</div>
|
||||
)}
|
||||
{!isOptions && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>时间:</div>
|
||||
<div className={itemValueClass}>
|
||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{['METRIC_GROUPBY', 'METRIC_ORDERBY', 'ENTITY_DETAIL'].includes(queryMode) &&
|
||||
fields &&
|
||||
fields.length > 0 && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>
|
||||
{queryMode === 'ENTITY_DETAIL' ? '查询字段' : '下钻维度'}:
|
||||
</div>
|
||||
<div className={itemValueClass}>
|
||||
{fields
|
||||
.slice(0, MAX_OPTION_VALUES_COUNT)
|
||||
.map(field => field.name)
|
||||
.join('、')}
|
||||
{fields.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{['METRIC_FILTER', 'METRIC_ENTITY', 'ENTITY_DETAIL', 'ENTITY_LIST_FILTER'].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>
|
||||
)}
|
||||
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
||||
<div className={itemValueClass}>{AGG_TYPE_MAP[aggType]}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
let tipNode: ReactNode;
|
||||
|
||||
if (parseInfoOptions.length > 1) {
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-multi-options`}>
|
||||
<div>您的问题解析为以下几项,请您点击确认</div>
|
||||
<div className={`${prefixCls}-options`}>
|
||||
{parseInfoOptions.map((item, index) => getTipNode(item, true, index))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const pluginName = parseInfoOptions[0]?.properties?.CONTEXT?.plugin?.name;
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
<div>{!!pluginName ? '您的问题' : '您的问题解析为:'}</div>
|
||||
{getTipNode(parseInfoOptions[0])}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text data={tipNode} />;
|
||||
};
|
||||
|
||||
export default ParseTip;
|
||||
@@ -1,71 +1,112 @@
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import { ChatContextType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Typing from './Typing';
|
||||
import ChatMsg from '../ChatMsg';
|
||||
import { chatQuery } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
||||
import Text from './Text';
|
||||
import Tools from '../Tools';
|
||||
import { chatExecute, chatParse, switchEntity } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||
import IconFont from '../IconFont';
|
||||
import ParseTip from './ParseTip';
|
||||
import ExecuteItem from './ExecuteItem';
|
||||
|
||||
type Props = {
|
||||
msg: string;
|
||||
followQuestions?: string[];
|
||||
conversationId?: number;
|
||||
domainId?: number;
|
||||
filter?: any[];
|
||||
isLastMessage?: boolean;
|
||||
msgData?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
onMsgDataLoaded?: (data: MsgDataType) => void;
|
||||
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
||||
onUpdateMessageScroll?: () => void;
|
||||
};
|
||||
|
||||
const ChatItem: React.FC<Props> = ({
|
||||
msg,
|
||||
followQuestions,
|
||||
conversationId,
|
||||
domainId,
|
||||
filter,
|
||||
isLastMessage,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
msgData,
|
||||
onMsgDataLoaded,
|
||||
onUpdateMessageScroll,
|
||||
}) => {
|
||||
const [data, setData] = useState<MsgDataType>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tip, setTip] = useState('');
|
||||
const [parseLoading, setParseLoading] = useState(false);
|
||||
const [parseInfo, setParseInfo] = useState<ChatContextType>();
|
||||
const [parseInfoOptions, setParseInfoOptions] = useState<ChatContextType[]>([]);
|
||||
const [parseTip, setParseTip] = useState('');
|
||||
const [executeLoading, setExecuteLoading] = useState(false);
|
||||
const [executeTip, setExecuteTip] = useState('');
|
||||
const [executeMode, setExecuteMode] = useState(false);
|
||||
const [entitySwitching, setEntitySwitching] = useState(false);
|
||||
|
||||
const [chartIndex, setChartIndex] = useState(0);
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
const updateData = (res: Result<MsgDataType>) => {
|
||||
if (res.code === 401 || res.code === 412) {
|
||||
setTip(res.msg);
|
||||
setExecuteTip(res.msg);
|
||||
return false;
|
||||
}
|
||||
if (res.code !== 200) {
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
setExecuteTip(SEARCH_EXCEPTION_TIP);
|
||||
return false;
|
||||
}
|
||||
const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
|
||||
const { queryColumns, queryResults, queryState, queryMode, response } = res.data || {};
|
||||
if (queryState !== 'SUCCESS') {
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
setExecuteTip(response && typeof response === 'string' ? response : SEARCH_EXCEPTION_TIP);
|
||||
return false;
|
||||
}
|
||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
|
||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'WEB_PAGE') {
|
||||
setData(res.data);
|
||||
setTip('');
|
||||
setExecuteTip('');
|
||||
return true;
|
||||
}
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
return false;
|
||||
setExecuteTip(SEARCH_EXCEPTION_TIP);
|
||||
return true;
|
||||
};
|
||||
|
||||
const onExecute = async (parseInfoValue: ChatContextType, isSwitch?: boolean) => {
|
||||
setExecuteMode(true);
|
||||
setExecuteLoading(true);
|
||||
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||
setExecuteLoading(false);
|
||||
const valid = updateData(data);
|
||||
if (onMsgDataLoaded && !isSwitch) {
|
||||
onMsgDataLoaded({ ...data.data, chatContext: parseInfoValue }, valid);
|
||||
}
|
||||
};
|
||||
|
||||
const onSendMsg = async () => {
|
||||
setLoading(true);
|
||||
const semanticRes = await chatQuery(msg, conversationId, domainId);
|
||||
updateData(semanticRes.data);
|
||||
if (onMsgDataLoaded) {
|
||||
onMsgDataLoaded(semanticRes.data.data);
|
||||
setParseLoading(true);
|
||||
const { data: parseData } = await chatParse(msg, conversationId, domainId, filter);
|
||||
setParseLoading(false);
|
||||
const { code, data } = parseData || {};
|
||||
const { state, selectedParses } = data || {};
|
||||
if (
|
||||
code !== 200 ||
|
||||
state === ParseStateEnum.FAILED ||
|
||||
selectedParses == null ||
|
||||
selectedParses.length === 0 ||
|
||||
(selectedParses.length === 1 &&
|
||||
!selectedParses[0]?.domainName &&
|
||||
!selectedParses[0]?.properties?.CONTEXT?.plugin?.name &&
|
||||
selectedParses[0]?.queryMode !== 'WEB_PAGE')
|
||||
) {
|
||||
setParseTip(PARSE_ERROR_TIP);
|
||||
return;
|
||||
}
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
setParseInfoOptions(selectedParses || []);
|
||||
if (selectedParses.length === 1) {
|
||||
const parseInfoValue = selectedParses[0];
|
||||
setParseInfo(parseInfoValue);
|
||||
onExecute(parseInfoValue);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,55 +114,66 @@ const ChatItem: React.FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
if (msgData) {
|
||||
setParseInfoOptions([msgData.chatContext]);
|
||||
setExecuteMode(true);
|
||||
updateData({ code: 200, data: msgData, msg: 'success' });
|
||||
} else if (msg) {
|
||||
onSendMsg();
|
||||
}
|
||||
}, [msg, msgData]);
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
const onSwitchEntity = async (entityId: string) => {
|
||||
setEntitySwitching(true);
|
||||
const res = await switchEntity(entityId, data?.chatContext?.domainId, conversationId || 0);
|
||||
setEntitySwitching(false);
|
||||
setData(res.data.data);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<Typing />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const onChangeChart = () => {
|
||||
setChartIndex(chartIndex + 1);
|
||||
};
|
||||
|
||||
if (tip) {
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<Text data={tip} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.queryMode === 'INSTRUCTION') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMetricCard =
|
||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||
data.queryResults?.length === 1;
|
||||
const onSelectParseInfo = async (parseInfoValue: ChatContextType) => {
|
||||
setParseInfo(parseInfoValue);
|
||||
onExecute(parseInfoValue, parseInfo !== undefined);
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<ChatMsg
|
||||
question={msg}
|
||||
followQuestions={followQuestions}
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
{!isMetricCard && (
|
||||
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
||||
)}
|
||||
<div className={`${prefixCls}-section`}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<ParseTip
|
||||
parseLoading={parseLoading}
|
||||
parseInfoOptions={parseInfoOptions}
|
||||
parseTip={parseTip}
|
||||
currentParseInfo={parseInfo}
|
||||
onSelectParseInfo={onSelectParseInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{executeMode && data?.queryMode !== 'WEB_PAGE' && (
|
||||
<div className={`${prefixCls}-section`}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<ExecuteItem
|
||||
question={msg}
|
||||
executeLoading={executeLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
isLastMessage={isLastMessage}
|
||||
triggerResize={triggerResize}
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onChangeChart={onChangeChart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,101 @@
|
||||
|
||||
.@{chat-item-prefix-cls} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
width: 100%;
|
||||
|
||||
&-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-content-text {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&-msg-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-multi-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
padding: 4px 0 12px;
|
||||
}
|
||||
|
||||
&-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
row-gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&-tip-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&-tip-content-option {
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border-color-base);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--chat-blue);
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-tip-content-option-disabled {
|
||||
cursor: auto;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color-secondary);
|
||||
border-color: var(--border-color-base);
|
||||
}
|
||||
}
|
||||
|
||||
&-tip-content-option-active {
|
||||
border-color: var(--chat-blue);
|
||||
color: var(--chat-blue);
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
&-tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-mode-name {
|
||||
margin-right: -10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-tip-item-value {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-tip-item-option {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
display: flex;
|
||||
@@ -41,7 +136,7 @@
|
||||
|
||||
&-typing-bubble {
|
||||
width: fit-content;
|
||||
padding: 16px !important;
|
||||
// padding: 16px !important;
|
||||
}
|
||||
|
||||
&-text-bubble {
|
||||
|
||||
@@ -31,7 +31,7 @@ const BarChart: React.FC<Props> = ({
|
||||
|
||||
const { queryColumns, queryResults, entityInfo, chatContext, queryMode } = data;
|
||||
|
||||
const { dateInfo } = chatContext || {};
|
||||
const { dateInfo, dimensionFilters } = chatContext || {};
|
||||
|
||||
const categoryColumnName =
|
||||
queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || '';
|
||||
@@ -51,13 +51,6 @@ const BarChart: React.FC<Props> = ({
|
||||
);
|
||||
const xData = data.map(item => item[categoryColumnName]);
|
||||
instanceObj.setOption({
|
||||
// legend: {
|
||||
// left: 0,
|
||||
// top: 0,
|
||||
// icon: 'rect',
|
||||
// itemWidth: 15,
|
||||
// itemHeight: 5,
|
||||
// },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
axisTick: {
|
||||
@@ -166,21 +159,43 @@ const BarChart: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const hasFilterSection = dimensionFilters?.length > 0;
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-bar`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${PREFIX_CLS}-bar-metric-name`}>{metricColumn?.name}</div>
|
||||
<FilterSection chatContext={chatContext} />
|
||||
<div className={`${prefixCls}-top-bar`}>
|
||||
<div className={`${prefixCls}-indicator-name`}>{metricColumn?.name}</div>
|
||||
{(hasFilterSection || drillDownDimension) && (
|
||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||
(
|
||||
<div className={`${prefixCls}-filter-section`}>
|
||||
<FilterSection chatContext={chatContext} entityInfo={entityInfo} />
|
||||
{drillDownDimension && (
|
||||
<div className={`${prefixCls}-filter-item`}>
|
||||
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||
<div className={`${prefixCls}-filter-item-value`}>{drillDownDimension.name}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{dateInfo && (
|
||||
<div className={`${PREFIX_CLS}-bar-date-range`}>
|
||||
<div className={`${prefixCls}-date-range`}>
|
||||
{dateInfo.startDate === dateInfo.endDate
|
||||
? dateInfo.startDate
|
||||
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
||||
</div>
|
||||
)}
|
||||
<Spin spinning={loading}>
|
||||
<div className={`${PREFIX_CLS}-bar-chart`} ref={chartRef} />
|
||||
<div className={`${prefixCls}-chart`} ref={chartRef} />
|
||||
</Spin>
|
||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||
{(queryMode === 'METRIC_DOMAIN' ||
|
||||
queryMode === 'METRIC_FILTER' ||
|
||||
queryMode === 'METRIC_GROUPBY') && (
|
||||
<DrillDownDimensions
|
||||
domainId={chatContext.domainId}
|
||||
drillDownDimension={drillDownDimension}
|
||||
|
||||
@@ -9,11 +9,49 @@
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&-metric-name {
|
||||
font-size: 15px;
|
||||
&-top-bar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 8px;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
&-filter-section-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-filter-item-label {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-item-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-indicator-name {
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&-date-range {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { ChatContextType } from '../../../common/type';
|
||||
import { ChatContextType, EntityInfoType } from '../../../common/type';
|
||||
|
||||
type Props = {
|
||||
chatContext?: ChatContextType;
|
||||
entityInfo?: EntityInfoType;
|
||||
};
|
||||
|
||||
const FilterSection: React.FC<Props> = ({ chatContext }) => {
|
||||
const FilterSection: React.FC<Props> = ({ chatContext, entityInfo }) => {
|
||||
const prefixCls = `${PREFIX_CLS}-filter-section`;
|
||||
|
||||
const entityInfoList =
|
||||
entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || [];
|
||||
|
||||
const { dimensionFilters } = chatContext || {};
|
||||
|
||||
const hasFilterSection = dimensionFilters && dimensionFilters.length > 0;
|
||||
@@ -16,7 +20,7 @@ const FilterSection: React.FC<Props> = ({ chatContext }) => {
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-field-label`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-filter-values`}>
|
||||
{dimensionFilters.map(filterItem => {
|
||||
{(entityInfoList.length > 0 ? entityInfoList : dimensionFilters).map(filterItem => {
|
||||
const filterValue =
|
||||
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
||||
return (
|
||||
|
||||
@@ -5,21 +5,25 @@
|
||||
.@{filter-section-prefix-cls} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 12px;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
|
||||
&-field-label {
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
&-filter-values {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
&-filter-item {
|
||||
padding: 2px 12px;
|
||||
color: var(--text-color-third);
|
||||
background-color: #edf2f2;
|
||||
border-radius: 13px;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -12,67 +12,28 @@ type Props = {
|
||||
entityInfo?: EntityInfoType;
|
||||
children?: React.ReactNode;
|
||||
isMobileMode?: boolean;
|
||||
queryMode?: string;
|
||||
};
|
||||
|
||||
const Message: React.FC<Props> = ({
|
||||
position,
|
||||
width,
|
||||
height,
|
||||
title,
|
||||
followQuestions,
|
||||
children,
|
||||
bubbleClassName,
|
||||
chatContext,
|
||||
entityInfo,
|
||||
isMobileMode,
|
||||
queryMode,
|
||||
chatContext,
|
||||
}) => {
|
||||
const { dimensionFilters, domainName } = chatContext || {};
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-message`;
|
||||
|
||||
const { domainName, dateInfo, dimensionFilters } = chatContext || {};
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
|
||||
const entityInfoList =
|
||||
entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || [];
|
||||
|
||||
const hasFilterSection =
|
||||
dimensionFilters && dimensionFilters.length > 0 && entityInfoList.length === 0;
|
||||
|
||||
const filterSection = hasFilterSection && (
|
||||
<div className={`${prefixCls}-filter-section`}>
|
||||
<div className={`${prefixCls}-field-name`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-filter-values`}>
|
||||
{dimensionFilters.map(filterItem => {
|
||||
const filterValue =
|
||||
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
||||
return (
|
||||
<div
|
||||
className={`${prefixCls}-filter-item`}
|
||||
key={filterItem.name}
|
||||
title={filterValue.join('、')}
|
||||
>
|
||||
{filterItem.name}:{filterValue.join('、')}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const leftTitle = title
|
||||
? followQuestions && followQuestions.length > 0
|
||||
? `多轮对话:${[title, ...followQuestions].join(' ← ')}`
|
||||
: `单轮对话:${title}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-title-bar`}>
|
||||
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
|
||||
{position === 'left' && leftTitle && (
|
||||
<div className={`${prefixCls}-top-bar`} title={leftTitle}>
|
||||
({leftTitle})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<div className={`${prefixCls}-body`}>
|
||||
<div
|
||||
@@ -82,10 +43,9 @@ const Message: React.FC<Props> = ({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{entityInfoList.length > 0 && (
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
{/* {filterSection} */}
|
||||
{entityInfoList.length > 0 && (
|
||||
{(queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL') &&
|
||||
entityInfoList.length > 0 && (
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
<div className={`${prefixCls}-main-entity-info`}>
|
||||
{entityInfoList.slice(0, 4).map(dimension => {
|
||||
return (
|
||||
@@ -100,7 +60,36 @@ const Message: React.FC<Props> = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{queryMode === 'ENTITY_LIST_FILTER' && (
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
<div className={`${prefixCls}-main-entity-info`}>
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
<div className={`${prefixCls}-info-name`}>主题域:</div>
|
||||
<div className={`${prefixCls}-info-value`}>{domainName}</div>
|
||||
</div>
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
<div className={`${prefixCls}-info-name`}>时间:</div>
|
||||
<div className={`${prefixCls}-info-value`}>
|
||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||
</div>
|
||||
</div>
|
||||
{dimensionFilters && dimensionFilters?.length > 0 && (
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
<div className={`${prefixCls}-info-name`}>筛选条件:</div>
|
||||
{dimensionFilters.map((filter, index) => (
|
||||
<div className={`${prefixCls}-info-value`}>
|
||||
<span>{filter.name}:</span>
|
||||
<span>
|
||||
{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}
|
||||
</span>
|
||||
{index !== dimensionFilters.length - 1 && <span>、</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${prefixCls}-children`}>{children}</div>
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
align-items: center;
|
||||
row-gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 12px;
|
||||
column-gap: 20px;
|
||||
color: var(--text-color-secondary);
|
||||
background: rgba(133, 156, 241, 0.1);
|
||||
@@ -96,7 +97,6 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 20px;
|
||||
row-gap: 10px;
|
||||
}
|
||||
@@ -107,11 +107,12 @@
|
||||
}
|
||||
|
||||
&-info-name {
|
||||
color: var(--text-color-third);
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
&-info-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatByThousandSeperator } from '../../../utils/utils';
|
||||
import { formatMetric } from '../../../utils/utils';
|
||||
import ApplyAuth from '../ApplyAuth';
|
||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||
import PeriodCompareItem from './PeriodCompareItem';
|
||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||
import { Spin } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import FilterSection from '../FilterSection';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
@@ -25,8 +26,8 @@ const MetricCard: React.FC<Props> = ({
|
||||
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const { dateInfo } = chatContext || {};
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
const { dateInfo, dimensionFilters } = chatContext || {};
|
||||
const { startDate } = dateInfo || {};
|
||||
|
||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
||||
@@ -37,19 +38,36 @@ const MetricCard: React.FC<Props> = ({
|
||||
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
||||
});
|
||||
|
||||
const hasFilterSection = dimensionFilters?.length > 0;
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||
<div className={`${prefixCls}-top-bar`}>
|
||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||
{(hasFilterSection || drillDownDimension) && (
|
||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||
(
|
||||
<div className={`${prefixCls}-filter-section`}>
|
||||
<FilterSection chatContext={chatContext} entityInfo={entityInfo} />
|
||||
{drillDownDimension && (
|
||||
<div className={`${prefixCls}-filter-item`}>
|
||||
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||
<div className={`${prefixCls}-filter-item-value`}>{drillDownDimension.name}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
<div className={indicatorClass}>
|
||||
<div className={`${prefixCls}-date-range`}>
|
||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||
</div>
|
||||
<div className={`${prefixCls}-date-range`}>{startDate}</div>
|
||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{formatByThousandSeperator(queryResults?.[0]?.[indicatorColumnName])}
|
||||
{formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'}
|
||||
</div>
|
||||
)}
|
||||
{metricInfos?.length > 0 && (
|
||||
|
||||
@@ -7,6 +7,41 @@
|
||||
height: 130px;
|
||||
row-gap: 4px;
|
||||
|
||||
&-top-bar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
&-filter-section-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-filter-item-label {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-item-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-indicator-name {
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
@@ -74,7 +109,7 @@
|
||||
|
||||
&-drill-down-dimensions {
|
||||
position: absolute;
|
||||
bottom: -38px;
|
||||
left: 0;
|
||||
bottom: -44px;
|
||||
left: -16;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatByThousandSeperator } from '../../../utils/utils';
|
||||
import { formatMetric } from '../../../utils/utils';
|
||||
import { AggregateInfoType } from '../../../common/type';
|
||||
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||
|
||||
@@ -18,7 +18,7 @@ const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-indicator`}>
|
||||
<div className={`${prefixCls}-date`}>{date}</div>
|
||||
<div className={`${prefixCls}-indicator-value`}>{formatByThousandSeperator(value)}</div>
|
||||
<div className={`${prefixCls}-indicator-value`}>{formatMetric(value)}</div>
|
||||
{metricInfos?.length > 0 && (
|
||||
<div className={`${prefixCls}-period-compare`}>
|
||||
{Object.keys(statistics).map((key: any) => (
|
||||
|
||||
@@ -46,8 +46,17 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const valueColumnName = metricField.nameEn;
|
||||
const groupDataValue = groupByColumn(resultList, categoryColumnName);
|
||||
const [startDate, endDate] = getMinMaxDate(resultList, dateColumnName);
|
||||
const dataSource = resultList.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
[dateColumnName]: Array.isArray(item[dateColumnName])
|
||||
? moment(item[dateColumnName].join('')).format('MM-DD')
|
||||
: item[dateColumnName],
|
||||
};
|
||||
});
|
||||
|
||||
const groupDataValue = groupByColumn(dataSource, categoryColumnName);
|
||||
const [startDate, endDate] = getMinMaxDate(dataSource, dateColumnName);
|
||||
const groupData = Object.keys(groupDataValue).reduce((result: any, key) => {
|
||||
result[key] =
|
||||
startDate &&
|
||||
@@ -61,7 +70,7 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
endDate,
|
||||
dateColumnName.includes('month') ? 'months' : 'days'
|
||||
)
|
||||
: groupDataValue[key].reverse();
|
||||
: groupDataValue[key];
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
@@ -167,7 +176,7 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
data: data.map((item: any) => {
|
||||
const value = item[valueColumnName];
|
||||
return metricField.dataFormatType === 'percent' &&
|
||||
metricField.dataFormat?.needmultiply100
|
||||
metricField.dataFormat?.needMultiply100
|
||||
? value * 100
|
||||
: value;
|
||||
}),
|
||||
|
||||
@@ -10,29 +10,33 @@ import Table from '../Table';
|
||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||
import MetricInfo from './MetricInfo';
|
||||
import FilterSection from '../FilterSection';
|
||||
import moment from 'moment';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
chartIndex: number;
|
||||
triggerResize?: boolean;
|
||||
onApplyAuth?: (domain: string) => void;
|
||||
};
|
||||
|
||||
const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApplyAuth }) => {
|
||||
const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data;
|
||||
|
||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
||||
const initialDateOption = dateOptions.find(
|
||||
(option: any) => option.value === chatContext?.dateInfo?.unit
|
||||
)?.value;
|
||||
const { dateMode, unit } = chatContext?.dateInfo || {};
|
||||
|
||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
||||
const initialDateOption = dateOptions.find((option: any) => {
|
||||
return dateMode === 'RECENT' && option.value === unit;
|
||||
})?.value;
|
||||
|
||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns || []);
|
||||
const currentMetricField = columns.find((column: any) => column.showType === 'NUMBER');
|
||||
|
||||
const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
|
||||
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
|
||||
const [dimensions, setDimensions] = useState<FieldType[]>(chatContext?.dimensions);
|
||||
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||
const [dateModeValue, setDateModeValue] = useState(dateMode);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const dateField: any = columns.find(
|
||||
@@ -46,6 +50,18 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
setDataSource(queryResults);
|
||||
}, [queryResults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryMode === 'METRIC_GROUPBY') {
|
||||
const dimensionValue = chatContext?.dimensions?.find(
|
||||
dimension => dimension.type === 'DIMENSION'
|
||||
);
|
||||
setDrillDownDimension(dimensionValue);
|
||||
setDimensions(
|
||||
chatContext?.dimensions?.filter(dimension => dimension.id !== dimensionValue?.id)
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onLoadData = async (value: any) => {
|
||||
setLoading(true);
|
||||
const { data } = await queryData({
|
||||
@@ -61,19 +77,13 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
|
||||
const selectDateOption = (dateOption: number) => {
|
||||
setCurrentDateOption(dateOption);
|
||||
const endDate = moment().subtract(1, 'days').format('YYYY-MM-DD');
|
||||
const startDate = moment(endDate)
|
||||
.subtract(dateOption - 1, 'days')
|
||||
.format('YYYY-MM-DD');
|
||||
setDateModeValue('RECENT');
|
||||
onLoadData({
|
||||
metrics: [activeMetricField],
|
||||
dimensions: drillDownDimension
|
||||
? [...(chatContext.dimensions || []), drillDownDimension]
|
||||
: undefined,
|
||||
dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined,
|
||||
dateInfo: {
|
||||
...chatContext?.dateInfo,
|
||||
startDate,
|
||||
endDate,
|
||||
dateMode: 'RECENT',
|
||||
unit: dateOption,
|
||||
},
|
||||
});
|
||||
@@ -82,10 +92,12 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
const onSwitchMetric = (metricField: FieldType) => {
|
||||
setActiveMetricField(metricField);
|
||||
onLoadData({
|
||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
||||
dimensions: drillDownDimension
|
||||
? [...(chatContext.dimensions || []), drillDownDimension]
|
||||
: undefined,
|
||||
dateInfo: {
|
||||
...chatContext.dateInfo,
|
||||
dateMode: dateModeValue,
|
||||
unit: currentDateOption || chatContext.dateInfo.unit,
|
||||
},
|
||||
dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined,
|
||||
metrics: [metricField],
|
||||
});
|
||||
};
|
||||
@@ -93,10 +105,13 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
||||
setDrillDownDimension(dimension);
|
||||
onLoadData({
|
||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
||||
dateInfo: {
|
||||
...chatContext.dateInfo,
|
||||
dateMode: dateModeValue,
|
||||
unit: currentDateOption || chatContext.dateInfo.unit,
|
||||
},
|
||||
metrics: [activeMetricField],
|
||||
dimensions:
|
||||
dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension],
|
||||
dimensions: dimension === undefined ? undefined : [...(dimensions || []), dimension],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -106,36 +121,58 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
||||
|
||||
const { dimensionFilters } = chatContext || {};
|
||||
|
||||
const hasFilterSection = dimensionFilters?.length > 0;
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-charts`}>
|
||||
{chatContext.metrics.length > 0 && (
|
||||
<div className={`${prefixCls}-metric-fields`}>
|
||||
{chatContext.metrics.map((metricField: FieldType) => {
|
||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
||||
[`${prefixCls}-metric-field-active`]:
|
||||
activeMetricField?.bizName === metricField.bizName &&
|
||||
chatContext.metrics.length > 1,
|
||||
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={metricFieldClass}
|
||||
key={metricField.bizName}
|
||||
onClick={() => {
|
||||
if (chatContext.metrics.length > 1) {
|
||||
onSwitchMetric(metricField);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{metricField.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={`${prefixCls}-top-bar`}>
|
||||
{chatContext.metrics.length > 0 && (
|
||||
<div className={`${prefixCls}-metric-fields`}>
|
||||
{chatContext.metrics.slice(0, 5).map((metricField: FieldType) => {
|
||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
||||
[`${prefixCls}-metric-field-active`]:
|
||||
activeMetricField?.bizName === metricField.bizName &&
|
||||
chatContext.metrics.length > 1,
|
||||
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={metricFieldClass}
|
||||
key={metricField.bizName}
|
||||
onClick={() => {
|
||||
if (chatContext.metrics.length > 1) {
|
||||
onSwitchMetric(metricField);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{metricField.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{(hasFilterSection || drillDownDimension) && (
|
||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||
(
|
||||
<div className={`${prefixCls}-filter-section`}>
|
||||
<FilterSection chatContext={chatContext} />
|
||||
{drillDownDimension && (
|
||||
<div className={`${prefixCls}-filter-item`}>
|
||||
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||
<div className={`${prefixCls}-filter-item-value`}>
|
||||
{drillDownDimension.name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{aggregateInfo?.metricInfos?.length > 0 && <MetricInfo aggregateInfo={aggregateInfo} />}
|
||||
<FilterSection chatContext={chatContext} />
|
||||
<div className={`${prefixCls}-date-options`}>
|
||||
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
||||
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
|
||||
@@ -164,7 +201,7 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
})}
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
{dataSource?.length === 1 ? (
|
||||
{dataSource?.length === 1 || chartIndex % 2 === 1 ? (
|
||||
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<MetricTrendChart
|
||||
@@ -178,7 +215,9 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||
{(queryMode === 'METRIC_DOMAIN' ||
|
||||
queryMode === 'METRIC_FILTER' ||
|
||||
queryMode === 'METRIC_GROUPBY') && (
|
||||
<DrillDownDimensions
|
||||
domainId={chatContext.domainId}
|
||||
drillDownDimension={drillDownDimension}
|
||||
|
||||
@@ -13,6 +13,41 @@
|
||||
width: 100%;
|
||||
row-gap: 4px;
|
||||
|
||||
&-top-bar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
&-filter-section-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-filter-item-label {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-filter-item-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -171,4 +206,3 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,11 @@
|
||||
|
||||
&-holder {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
// background-image: url(~./images/line_chart_holder.png);
|
||||
// background-repeat: no-repeat;
|
||||
// background-size: 100% 300px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
&-bar-chart-holder {
|
||||
margin-top: 20px;
|
||||
// background-image: url(~./images/bar_chart_holder.png);
|
||||
}
|
||||
|
||||
&-no-permission {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../../../common/type';
|
||||
|
||||
type Props = {
|
||||
infoType?: SemanticTypeEnum;
|
||||
};
|
||||
|
||||
const SemanticTypeTag: React.FC<Props> = ({ infoType = SemanticTypeEnum.METRIC }) => {
|
||||
return (
|
||||
<Tag
|
||||
color={
|
||||
infoType === SemanticTypeEnum.DIMENSION || infoType === SemanticTypeEnum.DOMAIN
|
||||
? 'blue'
|
||||
: infoType === SemanticTypeEnum.VALUE
|
||||
? 'geekblue'
|
||||
: 'orange'
|
||||
}
|
||||
>
|
||||
{SEMANTIC_TYPE_MAP[infoType]}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
export default SemanticTypeTag;
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Popover, message, Row, Col, Button, Spin } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SemanticTypeEnum } from '../../../common/type';
|
||||
import { queryMetricInfo } from '../../../service';
|
||||
import SemanticTypeTag from './SemanticTypeTag';
|
||||
import { isMobile } from '../../../utils/utils';
|
||||
import { CLS_PREFIX } from '../../../common/constants';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
classId?: number;
|
||||
infoType?: SemanticTypeEnum;
|
||||
uniqueId: string | number;
|
||||
onDetailBtnClick?: (data: any) => void;
|
||||
};
|
||||
|
||||
const SemanticInfoPopover: React.FC<Props> = ({
|
||||
classId,
|
||||
infoType,
|
||||
uniqueId,
|
||||
children,
|
||||
onDetailBtnClick,
|
||||
}) => {
|
||||
const [semanticInfo, setSemanticInfo] = useState<any>(undefined);
|
||||
const [popoverVisible, setPopoverVisible] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-semantic-info-popover`;
|
||||
|
||||
const text = (
|
||||
<Row>
|
||||
<Col flex="1">
|
||||
<SemanticTypeTag infoType={infoType} />
|
||||
</Col>
|
||||
{onDetailBtnClick && (
|
||||
<Col flex="0 1 40px">
|
||||
{semanticInfo && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onDetailBtnClick(semanticInfo);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
|
||||
const content = loading ? (
|
||||
<div className={`${prefixCls}-spin-box`}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span>{semanticInfo?.description || '暂无数据'}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const getMetricInfo = async () => {
|
||||
setLoading(true);
|
||||
const { data: resData } = await queryMetricInfo({
|
||||
classId,
|
||||
uniqueId,
|
||||
});
|
||||
const { code, data, msg } = resData;
|
||||
setLoading(false);
|
||||
if (code === '0') {
|
||||
setSemanticInfo({
|
||||
...data,
|
||||
semanticInfoType: SemanticTypeEnum.METRIC,
|
||||
});
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (popoverVisible && !semanticInfo) {
|
||||
getMetricInfo();
|
||||
}
|
||||
}, [popoverVisible]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="top"
|
||||
title={text}
|
||||
content={content}
|
||||
trigger="hover"
|
||||
open={classId && !isMobile ? undefined : false}
|
||||
onOpenChange={visible => {
|
||||
setPopoverVisible(visible);
|
||||
}}
|
||||
overlayClassName={prefixCls}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default SemanticInfoPopover;
|
||||
@@ -1,18 +0,0 @@
|
||||
@import '../../../styles/index.less';
|
||||
|
||||
@semantic-info-popover-cls: ~'@{supersonic-chat-prefix}-semantic-info-popover';
|
||||
|
||||
.semantic-info-popover-cls {
|
||||
max-width: 300px;
|
||||
&-spin-box {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.ant-popover-title{
|
||||
padding: 5px 8px 4px;
|
||||
}
|
||||
.ant-popover-inner-content {
|
||||
min-height: 60px;
|
||||
min-width: 185px;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,15 @@ import { Table as AntTable } from 'antd';
|
||||
import { MsgDataType } from '../../../common/type';
|
||||
import { CLS_PREFIX } from '../../../common/constants';
|
||||
import ApplyAuth from '../ApplyAuth';
|
||||
import { SizeType } from 'antd/es/config-provider/SizeContext';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
size?: SizeType;
|
||||
onApplyAuth?: (domain: string) => void;
|
||||
};
|
||||
|
||||
const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
||||
const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => {
|
||||
const { entityInfo, queryColumns, queryResults } = data;
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-table`;
|
||||
@@ -19,7 +21,7 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
||||
return {
|
||||
dataIndex: nameEn,
|
||||
key: nameEn,
|
||||
title: name,
|
||||
title: name || nameEn,
|
||||
render: (value: string | number) => {
|
||||
if (!authorized) {
|
||||
return (
|
||||
@@ -30,7 +32,7 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
||||
return (
|
||||
<div className={`${prefixCls}-formatted-value`}>
|
||||
{`${formatByDecimalPlaces(
|
||||
dataFormat?.needmultiply100 ? +value * 100 : value,
|
||||
dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||
dataFormat?.decimalPlaces || 2
|
||||
)}%`}
|
||||
</div>
|
||||
@@ -71,6 +73,7 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
||||
style={{ width: '100%' }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowClassName={getRowClassName}
|
||||
size={size}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,19 +10,13 @@ import { queryData } from '../../service';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
followQuestions?: string[];
|
||||
data: MsgDataType;
|
||||
chartIndex: number;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
};
|
||||
|
||||
const ChatMsg: React.FC<Props> = ({
|
||||
question,
|
||||
followQuestions,
|
||||
data,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
}) => {
|
||||
const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, triggerResize }) => {
|
||||
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
||||
|
||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||
@@ -41,7 +35,10 @@ const ChatMsg: React.FC<Props> = ({
|
||||
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
||||
|
||||
const isMetricCard =
|
||||
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && singleData;
|
||||
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') &&
|
||||
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
|
||||
|
||||
const isText = columns.length === 1 && columns[0].showType === 'CATEGORY' && singleData;
|
||||
|
||||
const onLoadData = async (value: any) => {
|
||||
setLoading(true);
|
||||
@@ -65,6 +62,13 @@ const ChatMsg: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const getMsgContent = () => {
|
||||
if (isText) {
|
||||
return (
|
||||
<div style={{ lineHeight: '24px', width: 'fit-content' }}>
|
||||
{dataSource[0][columns[0].nameEn]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isMetricCard) {
|
||||
return (
|
||||
<MetricCard
|
||||
@@ -88,6 +92,7 @@ const ChatMsg: React.FC<Props> = ({
|
||||
return (
|
||||
<MetricTrend
|
||||
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||
chartIndex={chartIndex}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
);
|
||||
@@ -105,7 +110,9 @@ const ChatMsg: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
let width = '100%';
|
||||
if (isMetricCard) {
|
||||
if (isText) {
|
||||
width = 'fit-content';
|
||||
} else if (isMetricCard) {
|
||||
width = '370px';
|
||||
} else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||
if (columns.length === 1) {
|
||||
@@ -121,9 +128,9 @@ const ChatMsg: React.FC<Props> = ({
|
||||
chatContext={chatContext}
|
||||
entityInfo={entityInfo}
|
||||
title={question}
|
||||
followQuestions={followQuestions}
|
||||
isMobileMode={isMobileMode}
|
||||
width={width}
|
||||
queryMode={queryMode}
|
||||
>
|
||||
{getMsgContent()}
|
||||
</Message>
|
||||
|
||||
@@ -14,7 +14,6 @@ type Props = {
|
||||
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_DIMENSION_COUNT = 5;
|
||||
const MAX_DIMENSION_COUNT = 20;
|
||||
|
||||
const DrillDownDimensions: React.FC<Props> = ({
|
||||
@@ -26,6 +25,8 @@ const DrillDownDimensions: React.FC<Props> = ({
|
||||
}) => {
|
||||
const [dimensions, setDimensions] = useState<DrillDownDimensionType[]>([]);
|
||||
|
||||
const DEFAULT_DIMENSION_COUNT = isMetricCard ? 3 : 5;
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`;
|
||||
|
||||
const initData = async () => {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { getFormattedValue, isMobile } from '../../utils/utils';
|
||||
import { Table } from 'antd';
|
||||
import Avatar from 'antd/lib/avatar/avatar';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { queryEntities } from '../../service';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
import IconFont from '../IconFont';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
entityId: string | number;
|
||||
domainId: number;
|
||||
domainName: string;
|
||||
isMobileMode?: boolean;
|
||||
onSelect: (option: string) => void;
|
||||
};
|
||||
|
||||
const RecommendOptions: React.FC<Props> = ({
|
||||
entityId,
|
||||
domainId,
|
||||
domainName,
|
||||
isMobileMode,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-recommend-options`;
|
||||
|
||||
const initData = async () => {
|
||||
setLoading(true);
|
||||
const res = await queryEntities(entityId, domainId);
|
||||
setLoading(false);
|
||||
setData(res.data.data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (entityId) {
|
||||
initData();
|
||||
}
|
||||
}, [entityId]);
|
||||
|
||||
const getSectionOptions = () => {
|
||||
const basicColumn = {
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
title: '基本信息',
|
||||
render: (name: string, record: any) => {
|
||||
return (
|
||||
<div className={`${prefixCls}-item-name-column`}>
|
||||
<Avatar
|
||||
shape="square"
|
||||
icon={<IconFont type={domainName === '艺人库' ? 'icon-geshou' : 'icon-zhuanji'} />}
|
||||
src={record.url}
|
||||
/>
|
||||
<div className={`${prefixCls}-entity-name`}>
|
||||
{name}
|
||||
{record.ver && record.ver !== '完整版' && record.ver !== '-' && `(${record.ver})`}
|
||||
{record.singerName && ` - ${record.singerName}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const playCntColumnIdex = domainName.includes('歌曲')
|
||||
? 'tme3platAvgLogYyPlayCnt'
|
||||
: 'tme3platJsPlayCnt';
|
||||
|
||||
const columns = isMobile
|
||||
? [basicColumn]
|
||||
: [
|
||||
basicColumn,
|
||||
domainName.includes('艺人')
|
||||
? {
|
||||
dataIndex: 'onlineSongCnt',
|
||||
key: 'onlineSongCnt',
|
||||
title: '在架歌曲数',
|
||||
align: 'center',
|
||||
render: (onlineSongCnt: string) => {
|
||||
return onlineSongCnt ? getFormattedValue(+onlineSongCnt) : '-';
|
||||
},
|
||||
}
|
||||
: {
|
||||
dataIndex: 'publishTime',
|
||||
key: 'publishTime',
|
||||
title: '发布时间',
|
||||
align: 'center',
|
||||
render: (publishTime: string) => {
|
||||
return publishTime ? moment(publishTime).format('YYYY-MM-DD') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: playCntColumnIdex,
|
||||
key: playCntColumnIdex,
|
||||
align: 'center',
|
||||
title: domainName.includes('歌曲') ? '近7天日均运营播放量' : '昨日结算播放量',
|
||||
render: (value: string) => {
|
||||
return value ? getFormattedValue(+value) : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns as any}
|
||||
dataSource={data}
|
||||
showHeader={!isMobile}
|
||||
size="small"
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
className={`${prefixCls}-table`}
|
||||
rowClassName={`${prefixCls}-table-row`}
|
||||
onRow={record => {
|
||||
return {
|
||||
onClick: () => {
|
||||
onSelect(record.id);
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const recommendOptionsClass = classNames(prefixCls, {
|
||||
[`${prefixCls}-mobile-mode`]: isMobileMode,
|
||||
});
|
||||
|
||||
return <div className={recommendOptionsClass}>{getSectionOptions()}</div>;
|
||||
};
|
||||
|
||||
export default RecommendOptions;
|
||||
@@ -0,0 +1,24 @@
|
||||
@import '../../styles/index.less';
|
||||
|
||||
@recommend-options-prefix-cls: ~'@{supersonic-chat-prefix}-recommend-options';
|
||||
|
||||
.@{recommend-options-prefix-cls} {
|
||||
padding: 8px 0 12px;
|
||||
|
||||
&-item-name-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
}
|
||||
|
||||
&-entity-name {
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-table-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { EntityInfoType } from '../../common/type';
|
||||
import Message from '../ChatMsg/Message';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
|
||||
type Props = {
|
||||
currentMsgAggregator?: string;
|
||||
columns: any[];
|
||||
mainEntity: EntityInfoType;
|
||||
suggestions: any;
|
||||
onSelect?: (value: string) => void;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = isMobile ? 3 : 5;
|
||||
|
||||
const Suggestion: React.FC<Props> = ({
|
||||
currentMsgAggregator,
|
||||
columns,
|
||||
mainEntity,
|
||||
suggestions,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [dimensions, setDimensions] = useState<string[]>([]);
|
||||
const [metrics, setMetrics] = useState<string[]>([]);
|
||||
const [dimensionIndex, setDimensionIndex] = useState(0);
|
||||
const [metricIndex, setMetricIndex] = useState(0);
|
||||
|
||||
const fields = columns
|
||||
.filter(column => currentMsgAggregator !== 'tag' || column.showType !== 'NUMBER')
|
||||
.concat(isMobile ? [] : mainEntity?.dimensions || [])
|
||||
.map(item => item.name);
|
||||
|
||||
useEffect(() => {
|
||||
setDimensions(
|
||||
suggestions.dimensions
|
||||
.filter((dimension: any) => !fields.some(field => field === dimension.name))
|
||||
.map((item: any) => item.name)
|
||||
);
|
||||
setMetrics(
|
||||
suggestions.metrics
|
||||
.filter((metric: any) => !fields.some(field => field === metric.name))
|
||||
.map((item: any) => item.name)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const reloadDimensionCmds = () => {
|
||||
const dimensionPageCount = Math.ceil(dimensions.length / PAGE_SIZE);
|
||||
setDimensionIndex((dimensionIndex + 1) % dimensionPageCount);
|
||||
};
|
||||
|
||||
const reloadMetricCmds = () => {
|
||||
const metricPageCount = Math.ceil(metrics.length / PAGE_SIZE);
|
||||
setMetricIndex((metricIndex + 1) % metricPageCount);
|
||||
};
|
||||
|
||||
const dimensionList = dimensions.slice(
|
||||
dimensionIndex * PAGE_SIZE,
|
||||
(dimensionIndex + 1) * PAGE_SIZE
|
||||
);
|
||||
|
||||
const metricList = metrics.slice(metricIndex * PAGE_SIZE, (metricIndex + 1) * PAGE_SIZE);
|
||||
|
||||
if (!dimensionList.length && !metricList.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-suggestion`;
|
||||
|
||||
const suggestionClass = classNames(prefixCls, {
|
||||
[`${prefixCls}-mobile`]: isMobile,
|
||||
});
|
||||
|
||||
const sectionItemClass = classNames({
|
||||
[`${prefixCls}-section-item-selectable`]: onSelect !== undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={suggestionClass}>
|
||||
<Message position="left" width="fit-content">
|
||||
<div className={`${prefixCls}-tip`}>问答支持多轮对话,您可以继续输入:</div>
|
||||
{metricList.length > 0 && (
|
||||
<div className={`${prefixCls}-content-section`}>
|
||||
<div className={`${prefixCls}-title`}>指标:</div>
|
||||
<div className={`${prefixCls}-section-items`}>
|
||||
{metricList.map((metric, index) => {
|
||||
let metricNode = (
|
||||
<div
|
||||
className={sectionItemClass}
|
||||
onClick={() => {
|
||||
if (onSelect) {
|
||||
onSelect(metric);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{metric}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{metricNode}
|
||||
{index < metricList.length - 1 && '、'}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{metrics.length > PAGE_SIZE && (
|
||||
<div
|
||||
className={`${prefixCls}-reload`}
|
||||
onClick={() => {
|
||||
reloadMetricCmds();
|
||||
}}
|
||||
>
|
||||
<ReloadOutlined className={`${prefixCls}-reload-icon`} />
|
||||
{!isMobile && <div className={`${prefixCls}-reload-label`}>换一批</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{dimensionList.length > 0 && (
|
||||
<div className={`${prefixCls}-content-section`}>
|
||||
<div className={`${prefixCls}-title`}>维度:</div>
|
||||
<div className={`${prefixCls}-section-items`}>
|
||||
{dimensionList.map((dimension, index) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={sectionItemClass}
|
||||
onClick={() => {
|
||||
if (onSelect) {
|
||||
onSelect(dimension);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dimension}
|
||||
</div>
|
||||
{index < dimensionList.length - 1 && '、'}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{dimensions.length > PAGE_SIZE && (
|
||||
<div
|
||||
className={`${prefixCls}-reload`}
|
||||
onClick={() => {
|
||||
reloadDimensionCmds();
|
||||
}}
|
||||
>
|
||||
<ReloadOutlined className={`${prefixCls}-reload-icon`} />
|
||||
{!isMobile && <div className={`${prefixCls}-reload-label`}>换一批</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Message>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Suggestion;
|
||||
@@ -1,59 +0,0 @@
|
||||
@import '../../styles/index.less';
|
||||
|
||||
@suggestion-prefix-cls: ~'@{supersonic-chat-prefix}-suggestion';
|
||||
|
||||
.@{suggestion-prefix-cls} {
|
||||
margin-top: 30px;
|
||||
|
||||
.@{suggestion-prefix-cls}-mobile {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&-content-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
&-section-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-section-item-selectable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-reload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 14px;
|
||||
margin-left: 20px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
column-gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-reload-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Modal, Input, message } from 'antd';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
import { useState } from 'react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
feedbackValue: string;
|
||||
onSubmit: (feedback: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const FeedbackModal: React.FC<Props> = ({ visible, feedbackValue, onSubmit, onClose }) => {
|
||||
const [feedback, setFeedback] = useState(feedbackValue);
|
||||
const prefixCls = `${CLS_PREFIX}-tools`;
|
||||
|
||||
const onOk = () => {
|
||||
if (feedback.trim() === '') {
|
||||
message.warning('请输入点评内容');
|
||||
return;
|
||||
}
|
||||
onSubmit(feedback);
|
||||
};
|
||||
|
||||
const onFeedbackChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setFeedback(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title="点评一下~"
|
||||
onOk={onOk}
|
||||
onCancel={onClose}
|
||||
okText="提交"
|
||||
cancelText="取消"
|
||||
>
|
||||
<div className={`${prefixCls}-feedback-item`}>
|
||||
<div className={`${prefixCls}-feedback-item-title`}>评价</div>
|
||||
<TextArea
|
||||
placeholder="请输入评价"
|
||||
rows={3}
|
||||
value={feedback}
|
||||
onChange={onFeedbackChange}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackModal;
|
||||
@@ -1,20 +1,49 @@
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
||||
import { Button, message } from 'antd';
|
||||
import { Button, Popover, message } from 'antd';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import RecommendOptions from '../RecommendOptions';
|
||||
import { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { updateQAFeedback } from '../../service';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
scoreValue?: number;
|
||||
isLastMessage?: boolean;
|
||||
isMobileMode?: boolean;
|
||||
onSwitchEntity: (entityId: string) => void;
|
||||
onChangeChart: () => void;
|
||||
};
|
||||
|
||||
const Tools: React.FC<Props> = ({ data, isLastMessage, isMobileMode }) => {
|
||||
const Tools: React.FC<Props> = ({
|
||||
data,
|
||||
scoreValue,
|
||||
isLastMessage,
|
||||
isMobileMode,
|
||||
onSwitchEntity,
|
||||
onChangeChart,
|
||||
}) => {
|
||||
const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false);
|
||||
const { queryColumns, queryResults, queryId, chatContext, queryMode } = data || {};
|
||||
const [score, setScore] = useState(scoreValue || 0);
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-tools`;
|
||||
|
||||
const noDashboard =
|
||||
(queryColumns?.length === 1 &&
|
||||
queryColumns[0].showType === 'CATEGORY' &&
|
||||
queryResults?.length === 1) ||
|
||||
(!queryMode.includes('METRIC') && !queryMode.includes('ENTITY'));
|
||||
|
||||
console.log(
|
||||
'chatContext?.properties?.CONTEXT?.plugin?.name',
|
||||
chatContext?.properties?.CONTEXT?.plugin?.name
|
||||
);
|
||||
|
||||
const changeChart = () => {
|
||||
message.info('正在开发中,敬请期待');
|
||||
onChangeChart();
|
||||
};
|
||||
|
||||
const addToDashboard = () => {
|
||||
@@ -22,32 +51,73 @@ const Tools: React.FC<Props> = ({ data, isLastMessage, isMobileMode }) => {
|
||||
};
|
||||
|
||||
const like = () => {
|
||||
message.info('正在开发中,敬请期待');
|
||||
setScore(5);
|
||||
updateQAFeedback(queryId, 5);
|
||||
};
|
||||
|
||||
const dislike = () => {
|
||||
message.info('正在开发中,敬请期待');
|
||||
setScore(1);
|
||||
updateQAFeedback(queryId, 1);
|
||||
};
|
||||
|
||||
const feedbackSection = isLastMessage && (
|
||||
<div className={`${prefixCls}-feedback`}>
|
||||
<div>这个回答正确吗?</div>
|
||||
<LikeOutlined className={`${prefixCls}-like`} onClick={like} />
|
||||
<DislikeOutlined className={`${prefixCls}-dislike`} onClick={dislike} />
|
||||
</div>
|
||||
);
|
||||
const switchEntity = (option: string) => {
|
||||
setRecommendOptionsOpen(false);
|
||||
onSwitchEntity(option);
|
||||
};
|
||||
|
||||
const likeClass = classNames(`${prefixCls}-like`, {
|
||||
[`${prefixCls}-feedback-active`]: score === 5,
|
||||
});
|
||||
const dislikeClass = classNames(`${prefixCls}-dislike`, {
|
||||
[`${prefixCls}-feedback-active`]: score === 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
{!isMobile && !isMobileMode && (
|
||||
{/* {isLastMessage && chatContext?.domainId && entityInfo?.entityId && (
|
||||
<Popover
|
||||
content={
|
||||
<RecommendOptions
|
||||
entityId={entityInfo.entityId}
|
||||
domainId={chatContext.domainId}
|
||||
domainName={chatContext.domainName}
|
||||
isMobileMode={isMobileMode}
|
||||
onSelect={switchEntity}
|
||||
/>
|
||||
}
|
||||
placement={isMobileMode ? 'top' : 'right'}
|
||||
trigger="click"
|
||||
open={recommendOptionsOpen}
|
||||
onOpenChange={open => setRecommendOptionsOpen(open)}
|
||||
>
|
||||
<Button shape="round">切换其他匹配内容</Button>
|
||||
</Popover>
|
||||
)} */}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<Button shape="round" onClick={changeChart}>
|
||||
切换图表
|
||||
</Button>
|
||||
<Button shape="round" onClick={addToDashboard}>
|
||||
加入看板
|
||||
</Button>
|
||||
{feedbackSection}
|
||||
{queryMode === 'METRIC_FILTER' && (
|
||||
<Button shape="round" onClick={changeChart}>
|
||||
切换图表
|
||||
</Button>
|
||||
)}
|
||||
{!noDashboard && (
|
||||
<Button shape="round" onClick={addToDashboard}>
|
||||
加入看板
|
||||
</Button>
|
||||
)}
|
||||
{isLastMessage && (
|
||||
<div className={`${prefixCls}-feedback`}>
|
||||
<div>这个回答正确吗?</div>
|
||||
<LikeOutlined className={likeClass} onClick={like} />
|
||||
<DislikeOutlined
|
||||
className={dislikeClass}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
dislike();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&-feedback-active {
|
||||
color: rgb(234, 197, 79);
|
||||
}
|
||||
|
||||
&-mobile-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -34,4 +38,20 @@
|
||||
&-feedback {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&-feedback-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&-feedback-item-title {
|
||||
width: 40px;
|
||||
margin-right: 20px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user