mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-15 14:36:47 +00:00
[feature](webapp) upgrade agent
This commit is contained in:
@@ -7,29 +7,23 @@ import WebPage from '../ChatMsg/WebPage';
|
||||
import Loading from './Loading';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
queryId?: number;
|
||||
executeLoading: boolean;
|
||||
entitySwitchLoading: boolean;
|
||||
chartIndex: number;
|
||||
executeTip?: string;
|
||||
data?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
onChangeChart: () => void;
|
||||
};
|
||||
|
||||
const ExecuteItem: React.FC<Props> = ({
|
||||
question,
|
||||
queryId,
|
||||
executeLoading,
|
||||
entitySwitchLoading,
|
||||
chartIndex,
|
||||
executeTip,
|
||||
data,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
onChangeChart,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
@@ -71,13 +65,7 @@ const ExecuteItem: React.FC<Props> = ({
|
||||
{data?.queryMode === 'WEB_PAGE' ? (
|
||||
<WebPage id={queryId!} data={data} />
|
||||
) : (
|
||||
<ChatMsg
|
||||
question={question}
|
||||
data={data}
|
||||
chartIndex={chartIndex}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
<ChatMsg data={data} chartIndex={chartIndex} triggerResize={triggerResize} />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Select, Spin } from 'antd';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { FilterItemType } from '../../common/type';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { queryDimensionValues } from '../../service';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
type Props = {
|
||||
modelId: number;
|
||||
filters: FilterItemType[];
|
||||
filter: FilterItemType;
|
||||
onFiltersChange: (filters: FilterItemType[]) => void;
|
||||
};
|
||||
|
||||
const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange }) => {
|
||||
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fetchRef = useRef(0);
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-filter-item`;
|
||||
|
||||
const initData = async () => {
|
||||
const { data } = await queryDimensionValues(modelId, filter.bizName, '');
|
||||
setOptions(
|
||||
data?.data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof filter.value === 'string' && options.length === 0) {
|
||||
initData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debounceFetcher = useMemo(() => {
|
||||
const loadOptions = (value: string) => {
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
setOptions([]);
|
||||
setLoading(true);
|
||||
|
||||
queryDimensionValues(modelId, filter.bizName, value).then(newOptions => {
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(
|
||||
newOptions.data?.data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return debounce(loadOptions, 800);
|
||||
}, [queryDimensionValues]);
|
||||
|
||||
const onChange = (value: string) => {
|
||||
const newFilters = filters.map(item => {
|
||||
if (item.bizName === filter.bizName) {
|
||||
item.value = `${value}`;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
onFiltersChange(newFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={prefixCls}>
|
||||
{typeof filter.value === 'string' ? (
|
||||
<Select
|
||||
bordered={false}
|
||||
value={filter.value}
|
||||
options={options}
|
||||
className={`${prefixCls}-select-control`}
|
||||
popupClassName={`${prefixCls}-select-popup`}
|
||||
onSearch={debounceFetcher}
|
||||
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||
onChange={onChange}
|
||||
showSearch
|
||||
/>
|
||||
) : (
|
||||
<span>{filter.value}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterItem;
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||
import { ChatContextType } from '../../common/type';
|
||||
import { ChatContextType, FilterItemType } from '../../common/type';
|
||||
import { CheckCircleFilled, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import SwicthEntity from './SwitchEntity';
|
||||
import { Tooltip } from 'antd';
|
||||
import Loading from './Loading';
|
||||
import FilterItem from './FilterItem';
|
||||
|
||||
type Props = {
|
||||
parseLoading: boolean;
|
||||
parseInfoOptions: ChatContextType[];
|
||||
parseTip: string;
|
||||
currentParseInfo?: ChatContextType;
|
||||
optionMode?: boolean;
|
||||
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
||||
onSwitchEntity: (entityId: string) => void;
|
||||
onFiltersChange: (filters: FilterItemType[]) => void;
|
||||
};
|
||||
|
||||
const MAX_OPTION_VALUES_COUNT = 2;
|
||||
@@ -24,9 +24,9 @@ const ParseTip: React.FC<Props> = ({
|
||||
parseInfoOptions,
|
||||
parseTip,
|
||||
currentParseInfo,
|
||||
optionMode,
|
||||
onSelectParseInfo,
|
||||
onSwitchEntity,
|
||||
onFiltersChange,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
@@ -62,6 +62,7 @@ const ParseTip: React.FC<Props> = ({
|
||||
|
||||
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||
const {
|
||||
modelId,
|
||||
modelName,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
@@ -73,27 +74,12 @@ const ParseTip: React.FC<Props> = ({
|
||||
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 itemValueClass = `${prefixCls}-tip-item-value`;
|
||||
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||
@@ -106,14 +92,22 @@ const ParseTip: React.FC<Props> = ({
|
||||
const getFilterContent = (filters: any) => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||
{filters.map((filter: any, index: number) => (
|
||||
<div className={itemValueClass}>
|
||||
{filters.map((filter: any) => (
|
||||
<div className={`${prefixCls}-tip-item-option`}>
|
||||
<span>
|
||||
{filter.name}
|
||||
<span className={`${prefixCls}-tip-item-filter-name`}>{filter.name}</span>
|
||||
{filter.operator !== '=' ? ` ${filter.operator} ` : ':'}
|
||||
</span>
|
||||
<span>{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}</span>
|
||||
{index !== filters.length - 1 && <span>、</span>}
|
||||
{queryMode !== 'DSL' && !filter.bizName?.includes('_id') ? (
|
||||
<FilterItem
|
||||
modelId={modelId}
|
||||
filters={dimensionFilters}
|
||||
filter={filter}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
) : (
|
||||
<span className={itemValueClass}>{filter.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -124,27 +118,16 @@ const ParseTip: React.FC<Props> = ({
|
||||
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 className={`${prefixCls}-tip-item-content`}>
|
||||
{getFilterContent(dimensionFilters)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tipContentClass}
|
||||
className={`${prefixCls}-tip-content`}
|
||||
onClick={() => {
|
||||
if (isOptions && currentParseInfo === undefined) {
|
||||
onSelectParseInfo(parseInfo);
|
||||
@@ -234,41 +217,45 @@ const ParseTip: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
let tipNode: ReactNode;
|
||||
const parseInfo = parseInfoOptions[0] || {};
|
||||
const { properties, entity, entityInfo, elementMatches, queryMode } = parseInfo || {};
|
||||
|
||||
if (parseInfoOptions.length > 1 || optionMode) {
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-multi-options`}>
|
||||
<div>
|
||||
还有以下的相关问题,<strong>请您点击提交</strong>
|
||||
</div>
|
||||
<div className={`${prefixCls}-options`}>
|
||||
{parseInfoOptions.map((item, index) => getTipNode(item, true, index))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
const { type } = properties || {};
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
{getTipNode(parseInfoOptions[0])}
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
<InfoCircleOutlined />
|
||||
<div>
|
||||
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||
const entityDimensions = entityInfo?.dimensions?.filter(
|
||||
item =>
|
||||
!['zyqk_song_id', 'song_name', 'singer_id'].includes(item.bizName) &&
|
||||
!(
|
||||
entityInfo?.dimensions?.some(dimension => dimension.bizName === 'singer_id') &&
|
||||
item.bizName === 'singer_name'
|
||||
)
|
||||
);
|
||||
|
||||
const tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
{getTipNode(parseInfo)}
|
||||
{queryMode !== 'ENTITY_ID' && entityDimensions?.length > 0 && (
|
||||
<div className={`${prefixCls}-entity-info`}>
|
||||
{entityDimensions.map(dimension => (
|
||||
<div className={`${prefixCls}-dimension-item`} key={dimension.itemId}>
|
||||
<div className={`${prefixCls}-dimension-name`}>{dimension.name}:</div>
|
||||
<div className={`${prefixCls}-dimension-value`}>{dimension.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
<InfoCircleOutlined />
|
||||
<div>
|
||||
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return getNode('意图解析结果', tipNode, true);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChatContextType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import { ChatContextType, FilterItemType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { chatExecute, chatParse, switchEntity } from '../../service';
|
||||
import { chatExecute, chatParse, queryData, switchEntity } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||
import IconFont from '../IconFont';
|
||||
import ParseTip from './ParseTip';
|
||||
@@ -17,7 +17,6 @@ type Props = {
|
||||
filter?: any[];
|
||||
isLastMessage?: boolean;
|
||||
msgData?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
isHistory?: boolean;
|
||||
triggerResize?: boolean;
|
||||
parseOptions?: ChatContextType[];
|
||||
@@ -32,7 +31,6 @@ const ChatItem: React.FC<Props> = ({
|
||||
agentId,
|
||||
filter,
|
||||
isLastMessage,
|
||||
isMobileMode,
|
||||
isHistory,
|
||||
triggerResize,
|
||||
msgData,
|
||||
@@ -77,10 +75,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
return true;
|
||||
};
|
||||
|
||||
const onExecute = async (
|
||||
parseInfoValue: ChatContextType,
|
||||
parseInfoOptions?: ChatContextType[]
|
||||
) => {
|
||||
const onExecute = async (parseInfoValue: ChatContextType) => {
|
||||
setExecuteMode(true);
|
||||
setExecuteLoading(true);
|
||||
try {
|
||||
@@ -88,24 +83,10 @@ const ChatItem: React.FC<Props> = ({
|
||||
setExecuteLoading(false);
|
||||
const valid = updateData(data);
|
||||
if (onMsgDataLoaded) {
|
||||
let parseOptions: ChatContextType[] = parseInfoOptions || [];
|
||||
if (
|
||||
parseInfoOptions &&
|
||||
parseInfoOptions.length > 1 &&
|
||||
(parseInfoOptions[0].queryMode.includes('METRIC') ||
|
||||
parseInfoOptions[0].queryMode.includes('ENTITY'))
|
||||
) {
|
||||
parseOptions = parseInfoOptions.filter(
|
||||
(item, index) =>
|
||||
index === 0 ||
|
||||
(!item.queryMode.includes('METRIC') && !item.queryMode.includes('ENTITY'))
|
||||
);
|
||||
}
|
||||
onMsgDataLoaded(
|
||||
{
|
||||
...data.data,
|
||||
chatContext: parseInfoValue,
|
||||
parseOptions: parseOptions.length > 1 ? parseOptions.slice(1) : undefined,
|
||||
},
|
||||
valid
|
||||
);
|
||||
@@ -141,7 +122,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
setParseInfoOptions(parseInfos || []);
|
||||
const parseInfoValue = parseInfos[0];
|
||||
setParseInfo(parseInfoValue);
|
||||
onExecute(parseInfoValue, parseInfos);
|
||||
onExecute(parseInfoValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -167,8 +148,15 @@ const ChatItem: React.FC<Props> = ({
|
||||
setParseInfoOptions([chatContext]);
|
||||
};
|
||||
|
||||
const onChangeChart = () => {
|
||||
setChartIndex(chartIndex + 1);
|
||||
const onFiltersChange = async (dimensionFilters: FilterItemType[]) => {
|
||||
setEntitySwitchLoading(true);
|
||||
const chatContextValue = { ...(parseInfoOptions[0] || {}), dimensionFilters };
|
||||
const res: any = await queryData(chatContextValue);
|
||||
setEntitySwitchLoading(false);
|
||||
const resChatContext = res.data?.data?.chatContext;
|
||||
setData({ ...(res.data?.data || {}), chatContext: resChatContext || chatContextValue });
|
||||
setParseInfo(resChatContext || chatContextValue);
|
||||
setParseInfoOptions([resChatContext || chatContextValue]);
|
||||
};
|
||||
|
||||
const onSelectParseInfo = async (parseInfoValue: ChatContextType) => {
|
||||
@@ -197,22 +185,19 @@ const ChatItem: React.FC<Props> = ({
|
||||
parseInfoOptions={parseOptions || parseInfoOptions.slice(0, 1)}
|
||||
parseTip={parseTip}
|
||||
currentParseInfo={parseInfo}
|
||||
optionMode={parseOptions !== undefined}
|
||||
onSelectParseInfo={onSelectParseInfo}
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
{executeMode && (
|
||||
<ExecuteItem
|
||||
question={msg}
|
||||
queryId={parseInfo?.queryId}
|
||||
executeLoading={executeLoading}
|
||||
entitySwitchLoading={entitySwitchLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
onChangeChart={onChangeChart}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import '../../styles/index.less';
|
||||
|
||||
@chat-item-prefix-cls: ~'@{supersonic-chat-prefix}-item';
|
||||
@filter-item-prefix-cls: ~'@{supersonic-chat-prefix}-filter-item';
|
||||
|
||||
.@{chat-item-prefix-cls} {
|
||||
display: flex;
|
||||
@@ -215,6 +216,12 @@
|
||||
&-tip-item-filter-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
&-tip-item-filter-name {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
&-mode-name {
|
||||
@@ -231,6 +238,29 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-entity-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-dimension-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-dimension-name {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-dimension-value {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-metric-info-list {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
@@ -263,3 +293,14 @@
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.@{filter-item-prefix-cls} {
|
||||
&-select-control {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import { formatByDecimalPlaces, formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import ApplyAuth from '../ApplyAuth';
|
||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||
import PeriodCompareItem from './PeriodCompareItem';
|
||||
@@ -27,12 +27,13 @@ const MetricCard: React.FC<Props> = ({
|
||||
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const { dateInfo } = chatContext || {};
|
||||
const { startDate } = dateInfo || {};
|
||||
|
||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
||||
|
||||
const { dataFormatType, dataFormat } = indicatorColumn || {};
|
||||
const value = queryResults?.[0]?.[indicatorColumnName] || 0;
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||
|
||||
const matricCardClass = classNames(prefixCls, {
|
||||
@@ -54,7 +55,7 @@ const MetricCard: React.FC<Props> = ({
|
||||
{indicatorColumn?.name ? (
|
||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||
) : (
|
||||
<div style={{ height: 6 }} />
|
||||
<div style={{ height: 10 }} />
|
||||
)}
|
||||
{drillDownDimension && (
|
||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||
@@ -73,19 +74,25 @@ const MetricCard: React.FC<Props> = ({
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
<div className={indicatorClass}>
|
||||
<div className={`${prefixCls}-date-range`}>{startDate}</div>
|
||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||
<ApplyAuth model={entityInfo?.modelInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{isNumber
|
||||
? formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'
|
||||
: formatNumberWithCN(+queryResults?.[0]?.[indicatorColumnName])}
|
||||
</div>
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
{dataFormatType === 'percent' || dataFormatType === 'decimal'
|
||||
? `${formatByDecimalPlaces(
|
||||
dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||
dataFormat?.decimalPlaces || 2
|
||||
)}${dataFormatType === 'percent' ? '%' : ''}`
|
||||
: isNumber
|
||||
? formatMetric(value) || '-'
|
||||
: formatNumberWithCN(+value)}
|
||||
</div>
|
||||
{!isNaN(+value) && +value >= 10000 && (
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{metricInfos?.length > 0 && (
|
||||
@@ -100,8 +107,8 @@ const MetricCard: React.FC<Props> = ({
|
||||
{queryMode.includes('METRIC') && (
|
||||
<div className={`${prefixCls}-drill-down-dimensions`}>
|
||||
<DrillDownDimensions
|
||||
modelId={chatContext.modelId}
|
||||
dimensionFilters={chatContext.dimensionFilters}
|
||||
modelId={chatContext?.modelId}
|
||||
dimensionFilters={chatContext?.dimensionFilters}
|
||||
drillDownDimension={drillDownDimension}
|
||||
onSelectDimension={onSelectDimension}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
.@{metric-card-prefix-cls} {
|
||||
width: 100%;
|
||||
height: 162px;
|
||||
row-gap: 4px;
|
||||
|
||||
&-dsl {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import { AggregateInfoType } from '../../../common/type';
|
||||
import { formatByDecimalPlaces, formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import { AggregateInfoType, ColumnType } from '../../../common/type';
|
||||
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||
import { SwapOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
aggregateInfo: AggregateInfoType;
|
||||
currentMetricField: ColumnType;
|
||||
};
|
||||
|
||||
const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||
const MetricInfo: React.FC<Props> = ({ aggregateInfo, currentMetricField }) => {
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const metricInfo = metricInfos?.[0] || {};
|
||||
const { date, value, statistics } = metricInfo || {};
|
||||
const { dataFormatType, dataFormat } = currentMetricField;
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-metric-info`;
|
||||
|
||||
@@ -26,11 +28,20 @@ const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||
<div className={`${prefixCls}-indicator`}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{isNumber ? formatMetric(value) : formatNumberWithCN(+value)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
{dataFormatType === 'percent' || dataFormatType === 'decimal'
|
||||
? `${formatByDecimalPlaces(
|
||||
dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||
dataFormat?.decimalPlaces || 2
|
||||
)}${dataFormatType === 'percent' ? '%' : ''}`
|
||||
: isNumber
|
||||
? formatMetric(value)
|
||||
: formatNumberWithCN(+value)}
|
||||
</div>
|
||||
{!isNaN(+value) && +value >= 10000 && (
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-bottom-section`}>
|
||||
<div className={`${prefixCls}-date`}>
|
||||
|
||||
@@ -147,11 +147,12 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
}</span><span style="display: inline-block; width: 90px; text-align: right; font-weight: 500;">${
|
||||
item.value === ''
|
||||
? '-'
|
||||
: metricField.dataFormatType === 'percent'
|
||||
: metricField.dataFormatType === 'percent' ||
|
||||
metricField.dataFormatType === 'decimal'
|
||||
? `${formatByDecimalPlaces(
|
||||
item.value,
|
||||
metricField.dataFormat?.decimalPlaces || 2
|
||||
)}%`
|
||||
)}${metricField.dataFormatType === 'percent' ? '%' : ''}`
|
||||
: getFormattedValue(item.value)
|
||||
}</span></div>`
|
||||
)
|
||||
@@ -176,7 +177,8 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
smooth: true,
|
||||
data: data.map((item: any) => {
|
||||
const value = item[valueColumnName];
|
||||
return metricField.dataFormatType === 'percent' &&
|
||||
return (metricField.dataFormatType === 'percent' ||
|
||||
metricField.dataFormatType === 'decimal') &&
|
||||
metricField.dataFormat?.needMultiply100
|
||||
? value * 100
|
||||
: value;
|
||||
|
||||
@@ -177,7 +177,10 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
||||
<Spin spinning={loading}>
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{!isMobile && aggregateInfoValue?.metricInfos?.length > 0 && (
|
||||
<MetricInfo aggregateInfo={aggregateInfoValue} />
|
||||
<MetricInfo
|
||||
aggregateInfo={aggregateInfoValue}
|
||||
currentMetricField={currentMetricField}
|
||||
/>
|
||||
)}
|
||||
<div className={`${prefixCls}-date-options`}>
|
||||
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
row-gap: 4px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
&-metric-fields {
|
||||
@@ -198,7 +198,7 @@
|
||||
&-indicator-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 36px;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
margin-top: 2px;
|
||||
color: var(--text-color-secondary);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import Bar from './Bar';
|
||||
import MetricCard from './MetricCard';
|
||||
import MetricTrend from './MetricTrend';
|
||||
@@ -10,14 +9,12 @@ import classNames from 'classnames';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
data: MsgDataType;
|
||||
chartIndex: number;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
};
|
||||
|
||||
const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, triggerResize }) => {
|
||||
const ChatMsg: React.FC<Props> = ({ data, chartIndex, triggerResize }) => {
|
||||
const { queryColumns, queryResults, chatContext, queryMode } = data;
|
||||
|
||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||
@@ -169,19 +166,6 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
||||
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
||||
};
|
||||
|
||||
// let width = '100%';
|
||||
// if (isText) {
|
||||
// width = 'fit-content';
|
||||
// } else if (isMetricCard) {
|
||||
// width = isDslMetricCard ? '290px' : '370px';
|
||||
// } else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||
// if (columns.length === 1) {
|
||||
// width = '600px';
|
||||
// } else if (columns.length === 2) {
|
||||
// width = '1000px';
|
||||
// }
|
||||
// }
|
||||
|
||||
const chartMsgClass = classNames({ [prefixCls]: !isTable });
|
||||
|
||||
return <div className={chartMsgClass}>{getMsgContent()}</div>;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
flex-wrap: wrap;
|
||||
column-gap: 6px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-metric-card {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_qiku6b2kol.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_sz2crkuyuj.js',
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
Reference in New Issue
Block a user