mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 19:51:00 +00:00
[feature](webapp) merge query steps to one card
This commit is contained in:
@@ -192,4 +192,4 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0"
|
"node": ">=14.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,10 @@ export type DateInfoType = {
|
|||||||
export type FilterItemType = {
|
export type FilterItemType = {
|
||||||
elementID: number;
|
elementID: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
bizName: string;
|
||||||
operator: string;
|
operator: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: string[];
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelType = {
|
export type ModelType = {
|
||||||
@@ -62,6 +63,8 @@ export type ModelType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ChatContextType = {
|
export type ChatContextType = {
|
||||||
|
id: number;
|
||||||
|
queryId: number;
|
||||||
aggType: string;
|
aggType: string;
|
||||||
modelId: number;
|
modelId: number;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
@@ -69,7 +72,7 @@ export type ChatContextType = {
|
|||||||
dateInfo: DateInfoType;
|
dateInfo: DateInfoType;
|
||||||
dimensions: FieldType[];
|
dimensions: FieldType[];
|
||||||
metrics: FieldType[];
|
metrics: FieldType[];
|
||||||
entity: { alias: string[] };
|
entity: { alias: string[], id: number };
|
||||||
elementMatches: any[];
|
elementMatches: any[];
|
||||||
queryMode: string;
|
queryMode: string;
|
||||||
dimensionFilters: FilterItemType[];
|
dimensionFilters: FilterItemType[];
|
||||||
@@ -126,6 +129,7 @@ export enum ParseStateEnum {
|
|||||||
|
|
||||||
export type ParseDataType = {
|
export type ParseDataType = {
|
||||||
chatId: number;
|
chatId: number;
|
||||||
|
queryId: number;
|
||||||
queryText: string;
|
queryText: string;
|
||||||
state: ParseStateEnum;
|
state: ParseStateEnum;
|
||||||
selectedParses: ChatContextType[];
|
selectedParses: ChatContextType[];
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
|
import { CheckCircleFilled } from '@ant-design/icons';
|
||||||
import { PREFIX_CLS } from '../../common/constants';
|
import { PREFIX_CLS } from '../../common/constants';
|
||||||
import { MsgDataType } from '../../common/type';
|
import { MsgDataType } from '../../common/type';
|
||||||
import ChatMsg from '../ChatMsg';
|
import ChatMsg from '../ChatMsg';
|
||||||
import Tools from '../Tools';
|
import WebPage from '../ChatMsg/WebPage';
|
||||||
import Text from './Text';
|
import Loading from './Loading';
|
||||||
import Typing from './Typing';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
question: string;
|
question: string;
|
||||||
|
queryId?: number;
|
||||||
executeLoading: boolean;
|
executeLoading: boolean;
|
||||||
entitySwitchLoading: boolean;
|
entitySwitchLoading: boolean;
|
||||||
chartIndex: number;
|
chartIndex: number;
|
||||||
@@ -15,13 +16,12 @@ type Props = {
|
|||||||
data?: MsgDataType;
|
data?: MsgDataType;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
isLastMessage?: boolean;
|
|
||||||
onSwitchEntity: (entityId: string) => void;
|
|
||||||
onChangeChart: () => void;
|
onChangeChart: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExecuteItem: React.FC<Props> = ({
|
const ExecuteItem: React.FC<Props> = ({
|
||||||
question,
|
question,
|
||||||
|
queryId,
|
||||||
executeLoading,
|
executeLoading,
|
||||||
entitySwitchLoading,
|
entitySwitchLoading,
|
||||||
chartIndex,
|
chartIndex,
|
||||||
@@ -29,49 +29,59 @@ const ExecuteItem: React.FC<Props> = ({
|
|||||||
data,
|
data,
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
triggerResize,
|
triggerResize,
|
||||||
isLastMessage,
|
|
||||||
onSwitchEntity,
|
|
||||||
onChangeChart,
|
onChangeChart,
|
||||||
}) => {
|
}) => {
|
||||||
const prefixCls = `${PREFIX_CLS}-item`;
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
|
||||||
|
const getNodeTip = (title: string, tip?: string) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`${prefixCls}-title-bar`}>
|
||||||
|
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
||||||
|
<div className={`${prefixCls}-step-title`}>
|
||||||
|
{title}
|
||||||
|
{!tip && <Loading />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tip && <div className={`${prefixCls}-content-container`}>{tip}</div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (executeLoading) {
|
if (executeLoading) {
|
||||||
return <Typing />;
|
return getNodeTip('数据查询中');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executeTip) {
|
if (executeTip) {
|
||||||
return <Text data={executeTip} />;
|
return getNodeTip('数据查询失败', executeTip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || data.queryMode === 'WEB_PAGE') {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMetricCard =
|
|
||||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
|
||||||
data.queryResults?.length === 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${prefixCls}-msg-content`}>
|
<>
|
||||||
<Spin spinning={entitySwitchLoading}>
|
<div className={`${prefixCls}-title-bar`}>
|
||||||
<ChatMsg
|
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
||||||
question={question}
|
<div className={`${prefixCls}-step-title`}>数据查询结果</div>
|
||||||
data={data}
|
</div>
|
||||||
chartIndex={chartIndex}
|
<div className={`${prefixCls}-content-container ${prefixCls}-last-node`}>
|
||||||
isMobileMode={isMobileMode}
|
<Spin spinning={entitySwitchLoading}>
|
||||||
triggerResize={triggerResize}
|
{data?.queryMode === 'WEB_PAGE' ? (
|
||||||
/>
|
<WebPage id={queryId!} data={data} />
|
||||||
</Spin>
|
) : (
|
||||||
{!isMetricCard && (
|
<ChatMsg
|
||||||
<Tools
|
question={question}
|
||||||
data={data}
|
data={data}
|
||||||
isLastMessage={isLastMessage}
|
chartIndex={chartIndex}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
onSwitchEntity={onSwitchEntity}
|
triggerResize={triggerResize}
|
||||||
onChangeChart={onChangeChart}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
14
webapp/packages/chat-sdk/src/components/ChatItem/Loading.tsx
Normal file
14
webapp/packages/chat-sdk/src/components/ChatItem/Loading.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { PREFIX_CLS } from '../../common/constants';
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
return (
|
||||||
|
<span className={`${prefixCls}-loading`}>
|
||||||
|
<span className={`${prefixCls}-loading-dot`} />
|
||||||
|
<span className={`${prefixCls}-loading-dot`} />
|
||||||
|
<span className={`${prefixCls}-loading-dot`} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||||
import { ChatContextType } from '../../common/type';
|
import { ChatContextType } from '../../common/type';
|
||||||
import Text from './Text';
|
import { CheckCircleFilled, InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import Typing from './Typing';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import SwicthEntity from './SwitchEntity';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import Loading from './Loading';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parseLoading: boolean;
|
parseLoading: boolean;
|
||||||
@@ -12,6 +14,7 @@ type Props = {
|
|||||||
currentParseInfo?: ChatContextType;
|
currentParseInfo?: ChatContextType;
|
||||||
optionMode?: boolean;
|
optionMode?: boolean;
|
||||||
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
||||||
|
onSwitchEntity: (entityId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_OPTION_VALUES_COUNT = 2;
|
const MAX_OPTION_VALUES_COUNT = 2;
|
||||||
@@ -23,15 +26,34 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
currentParseInfo,
|
currentParseInfo,
|
||||||
optionMode,
|
optionMode,
|
||||||
onSelectParseInfo,
|
onSelectParseInfo,
|
||||||
|
onSwitchEntity,
|
||||||
}) => {
|
}) => {
|
||||||
const prefixCls = `${PREFIX_CLS}-item`;
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
|
||||||
|
const getNode = (tipTitle: string, tipNode?: ReactNode, parseSucceed?: boolean) => {
|
||||||
|
const contentContainerClass = classNames(`${prefixCls}-content-container`, {
|
||||||
|
[`${prefixCls}-content-container-succeed`]: parseSucceed,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={`${prefixCls}-parse-tip`}>
|
||||||
|
<div className={`${prefixCls}-title-bar`}>
|
||||||
|
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
||||||
|
<div className={`${prefixCls}-step-title`}>
|
||||||
|
{tipTitle}
|
||||||
|
{!tipNode && <Loading />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tipNode && <div className={contentContainerClass}>{tipNode}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (parseLoading) {
|
if (parseLoading) {
|
||||||
return <Typing />;
|
return getNode('意图解析中');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseTip) {
|
if (parseTip) {
|
||||||
return <Text data={parseTip} />;
|
return getNode('意图解析失败', parseTip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInfoOptions.length === 0) {
|
if (parseInfoOptions.length === 0) {
|
||||||
@@ -81,6 +103,45 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
const fields =
|
const fields =
|
||||||
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
||||||
|
|
||||||
|
const getFilterContent = (filters: any) => {
|
||||||
|
return (
|
||||||
|
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||||
|
{filters.map((filter: any, index: number) => (
|
||||||
|
<div className={itemValueClass}>
|
||||||
|
<span>
|
||||||
|
{filter.name}
|
||||||
|
{filter.operator !== '=' ? ` ${filter.operator} ` : ':'}
|
||||||
|
</span>
|
||||||
|
<span>{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}</span>
|
||||||
|
{index !== filters.length - 1 && <span>、</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFiltersNode = () => {
|
||||||
|
return (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
dimensionFilters.length > MAX_OPTION_VALUES_COUNT
|
||||||
|
? getFilterContent(dimensionFilters)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
color="#fff"
|
||||||
|
overlayStyle={{ maxWidth: 'none' }}
|
||||||
|
>
|
||||||
|
<div className={`${prefixCls}-tip-item-content`}>
|
||||||
|
{getFilterContent(dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT))}
|
||||||
|
{dimensionFilters.length > MAX_OPTION_VALUES_COUNT && ' ...'}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tipContentClass}
|
className={tipContentClass}
|
||||||
@@ -91,20 +152,28 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{index !== undefined && <div>{index + 1}.</div>}
|
{index !== undefined && <div>{index + 1}.</div>}
|
||||||
{!!agentType ? (
|
{!!agentType && queryMode !== 'DSL' ? (
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
将由{agentType === 'plugin' ? '插件' : '内置'}工具
|
将由{agentType === 'plugin' ? '插件' : '内置'}工具
|
||||||
<span className={itemValueClass}>{agentName}</span>来解答
|
<span className={itemValueClass}>{agentName}</span>来解答
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{queryMode.includes('ENTITY') &&
|
{(queryMode.includes('ENTITY') || queryMode === 'DSL') &&
|
||||||
typeof entityId === 'string' &&
|
typeof entityId === 'string' &&
|
||||||
!!entityAlias &&
|
!!entityAlias &&
|
||||||
!!entityName ? (
|
!!entityName ? (
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}:</div>
|
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}:</div>
|
||||||
<div className={itemValueClass}>{entityName}</div>
|
{!isOptions && (entityAlias === '歌曲' || entityAlias === '艺人') ? (
|
||||||
|
<SwicthEntity
|
||||||
|
entityName={entityName}
|
||||||
|
chatContext={parseInfo}
|
||||||
|
onSwitchEntity={onSwitchEntity}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={itemValueClass}>{entityName}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
@@ -112,7 +181,7 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
<div className={itemValueClass}>{modelName}</div>
|
<div className={itemValueClass}>{modelName}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{metric && (
|
{queryMode !== 'ENTITY_ID' && metric && (
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
||||||
<div className={itemValueClass}>{metric.name}</div>
|
<div className={itemValueClass}>{metric.name}</div>
|
||||||
@@ -120,7 +189,7 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{!isOptions && (
|
{!isOptions && (
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
<div className={`${prefixCls}-tip-item-name`}>时间:</div>
|
<div className={`${prefixCls}-tip-item-name`}>数据时间:</div>
|
||||||
<div className={itemValueClass}>
|
<div className={itemValueClass}>
|
||||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||||
</div>
|
</div>
|
||||||
@@ -148,23 +217,11 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
'ENTITY_DETAIL',
|
'ENTITY_DETAIL',
|
||||||
'ENTITY_LIST_FILTER',
|
'ENTITY_LIST_FILTER',
|
||||||
'ENTITY_ID',
|
'ENTITY_ID',
|
||||||
|
'DSL',
|
||||||
].includes(queryMode) &&
|
].includes(queryMode) &&
|
||||||
dimensionFilters &&
|
dimensionFilters &&
|
||||||
dimensionFilters?.length > 0 && (
|
dimensionFilters?.length > 0 &&
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
getFiltersNode()}
|
||||||
<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' && (
|
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
|
||||||
<div className={`${prefixCls}-tip-item`}>
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
||||||
@@ -191,16 +248,29 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const agentType = parseInfoOptions[0]?.properties?.type;
|
const { type } = parseInfoOptions[0]?.properties || {};
|
||||||
|
const entityAlias = parseInfoOptions[0]?.entity?.alias?.[0]?.split('.')?.[0];
|
||||||
|
const entityName = parseInfoOptions[0]?.elementMatches?.find(
|
||||||
|
item => item.element?.type === 'ID'
|
||||||
|
)?.element.name;
|
||||||
|
const queryMode = parseInfoOptions[0]?.queryMode;
|
||||||
|
|
||||||
tipNode = (
|
tipNode = (
|
||||||
<div className={`${prefixCls}-tip`}>
|
<div className={`${prefixCls}-tip`}>
|
||||||
<div>{!!agentType ? '您的问题' : '您的问题解析为:'}</div>
|
|
||||||
{getTipNode(parseInfoOptions[0])}
|
{getTipNode(parseInfoOptions[0])}
|
||||||
|
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||||
|
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
<div>
|
||||||
|
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Text data={tipNode} />;
|
return getNode('意图解析结果', tipNode, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ParseTip;
|
export default ParseTip;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { ChatContextType } from '../../common/type';
|
||||||
|
import { Popover } from 'antd';
|
||||||
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
|
import RecommendOptions from '../RecommendOptions';
|
||||||
|
import { PREFIX_CLS } from '../../common/constants';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entityName: string;
|
||||||
|
chatContext: ChatContextType;
|
||||||
|
onSwitchEntity: (entityId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SwicthEntity: React.FC<Props> = ({ entityName, chatContext, onSwitchEntity }) => {
|
||||||
|
const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false);
|
||||||
|
const { modelId, modelName, dimensionFilters } = chatContext || {};
|
||||||
|
|
||||||
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
|
||||||
|
const switchEntity = (option: string) => {
|
||||||
|
setRecommendOptionsOpen(false);
|
||||||
|
onSwitchEntity(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
const entityId = dimensionFilters?.find(
|
||||||
|
filter => filter?.bizName === 'zyqk_song_id' || filter?.bizName === 'singer_id'
|
||||||
|
)?.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<RecommendOptions
|
||||||
|
entityId={entityId!}
|
||||||
|
modelId={modelId}
|
||||||
|
modelName={modelName}
|
||||||
|
onSelect={switchEntity}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="bottomLeft"
|
||||||
|
trigger="click"
|
||||||
|
open={recommendOptionsOpen}
|
||||||
|
onOpenChange={open => setRecommendOptionsOpen(open)}
|
||||||
|
>
|
||||||
|
<div className={`${prefixCls}-tip-item-value ${prefixCls}-switch-entity`}>
|
||||||
|
{entityName}
|
||||||
|
<DownOutlined className={`${prefixCls}-down-icon`} />
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SwicthEntity;
|
||||||
@@ -7,6 +7,7 @@ import ParseTip from './ParseTip';
|
|||||||
import ExecuteItem from './ExecuteItem';
|
import ExecuteItem from './ExecuteItem';
|
||||||
import { isMobile } from '../../utils/utils';
|
import { isMobile } from '../../utils/utils';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import Tools from '../Tools';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
msg: string;
|
msg: string;
|
||||||
@@ -17,6 +18,7 @@ type Props = {
|
|||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
msgData?: MsgDataType;
|
msgData?: MsgDataType;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
|
isHistory?: boolean;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
parseOptions?: ChatContextType[];
|
parseOptions?: ChatContextType[];
|
||||||
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
||||||
@@ -31,6 +33,7 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
filter,
|
filter,
|
||||||
isLastMessage,
|
isLastMessage,
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
|
isHistory,
|
||||||
triggerResize,
|
triggerResize,
|
||||||
msgData,
|
msgData,
|
||||||
parseOptions,
|
parseOptions,
|
||||||
@@ -118,15 +121,12 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
const { data: parseData } = await chatParse(msg, conversationId, modelId, agentId, filter);
|
const { data: parseData } = await chatParse(msg, conversationId, modelId, agentId, filter);
|
||||||
setParseLoading(false);
|
setParseLoading(false);
|
||||||
const { code, data } = parseData || {};
|
const { code, data } = parseData || {};
|
||||||
const { state, selectedParses } = data || {};
|
const { state, selectedParses, queryId } = data || {};
|
||||||
if (
|
if (
|
||||||
code !== 200 ||
|
code !== 200 ||
|
||||||
state === ParseStateEnum.FAILED ||
|
state === ParseStateEnum.FAILED ||
|
||||||
selectedParses == null ||
|
!selectedParses?.length ||
|
||||||
selectedParses.length === 0 ||
|
(!selectedParses[0]?.properties?.type && !selectedParses[0]?.queryMode)
|
||||||
(selectedParses.length > 0 &&
|
|
||||||
!selectedParses[0]?.properties?.type &&
|
|
||||||
!selectedParses[0]?.queryMode)
|
|
||||||
) {
|
) {
|
||||||
setParseTip(PARSE_ERROR_TIP);
|
setParseTip(PARSE_ERROR_TIP);
|
||||||
return;
|
return;
|
||||||
@@ -134,10 +134,14 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
if (onUpdateMessageScroll) {
|
if (onUpdateMessageScroll) {
|
||||||
onUpdateMessageScroll();
|
onUpdateMessageScroll();
|
||||||
}
|
}
|
||||||
setParseInfoOptions(selectedParses || []);
|
const parseInfos = selectedParses.map(item => ({
|
||||||
const parseInfoValue = selectedParses[0];
|
...item,
|
||||||
|
queryId,
|
||||||
|
}));
|
||||||
|
setParseInfoOptions(parseInfos || []);
|
||||||
|
const parseInfoValue = parseInfos[0];
|
||||||
setParseInfo(parseInfoValue);
|
setParseInfo(parseInfoValue);
|
||||||
onExecute(parseInfoValue, selectedParses);
|
onExecute(parseInfoValue, parseInfos);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -158,6 +162,9 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
|
const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
|
||||||
setEntitySwitchLoading(false);
|
setEntitySwitchLoading(false);
|
||||||
setData(res.data.data);
|
setData(res.data.data);
|
||||||
|
const { chatContext } = res.data.data;
|
||||||
|
setParseInfo(chatContext);
|
||||||
|
setParseInfoOptions([chatContext]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeChart = () => {
|
const onChangeChart = () => {
|
||||||
@@ -176,10 +183,14 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
[`${prefixCls}-content-mobile`]: isMobile,
|
[`${prefixCls}-content-mobile`]: isMobile,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMetricCard =
|
||||||
|
(data?.queryMode === 'METRIC_DOMAIN' || data?.queryMode === 'METRIC_FILTER') &&
|
||||||
|
data?.queryResults?.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-section`}>
|
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
|
||||||
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
|
<div className={isMobile ? `${prefixCls}-mobile-msg-card` : `${prefixCls}-msg-card`}>
|
||||||
<div className={contentClass}>
|
<div className={contentClass}>
|
||||||
<ParseTip
|
<ParseTip
|
||||||
parseLoading={parseLoading}
|
parseLoading={parseLoading}
|
||||||
@@ -188,29 +199,27 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
currentParseInfo={parseInfo}
|
currentParseInfo={parseInfo}
|
||||||
optionMode={parseOptions !== undefined}
|
optionMode={parseOptions !== undefined}
|
||||||
onSelectParseInfo={onSelectParseInfo}
|
onSelectParseInfo={onSelectParseInfo}
|
||||||
|
onSwitchEntity={onSwitchEntity}
|
||||||
/>
|
/>
|
||||||
</div>
|
{executeMode && (
|
||||||
</div>
|
|
||||||
{executeMode && data?.queryMode !== 'WEB_PAGE' && (
|
|
||||||
<div className={`${prefixCls}-section`}>
|
|
||||||
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
|
|
||||||
<div className={contentClass}>
|
|
||||||
<ExecuteItem
|
<ExecuteItem
|
||||||
question={msg}
|
question={msg}
|
||||||
|
queryId={parseInfo?.queryId}
|
||||||
executeLoading={executeLoading}
|
executeLoading={executeLoading}
|
||||||
entitySwitchLoading={entitySwitchLoading}
|
entitySwitchLoading={entitySwitchLoading}
|
||||||
executeTip={executeTip}
|
executeTip={executeTip}
|
||||||
chartIndex={chartIndex}
|
chartIndex={chartIndex}
|
||||||
data={data}
|
data={data}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
isLastMessage={isLastMessage}
|
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
onSwitchEntity={onSwitchEntity}
|
|
||||||
onChangeChart={onChangeChart}
|
onChangeChart={onChangeChart}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{!isMetricCard && data && (
|
||||||
|
<Tools data={data} scoreValue={undefined} isLastMessage={isLastMessage} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,23 +4,144 @@
|
|||||||
|
|
||||||
.@{chat-item-prefix-cls} {
|
.@{chat-item-prefix-cls} {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
row-gap: 20px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&-section {
|
&-loading {
|
||||||
width: 100%;
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-loading-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
// border-radius: 50%;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0;
|
||||||
|
animation: dot 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-loading-dot:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-loading-dot:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-loading-dot:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dot {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-avatar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--chat-blue);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-mobile-msg-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-msg-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 1px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content-mobile {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content-text {
|
&-content-text {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-msg-content {
|
&-title-bar {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
|
column-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-step-icon {
|
||||||
|
color: var(--green);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content-container {
|
||||||
|
margin: 2px 0 2px 7px;
|
||||||
|
padding: 6px 0 4px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content-container-succeed {
|
||||||
|
border-left: 1px solid var(--green);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-switch-entity-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-switch-entity {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-down-icon {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-last-node {
|
||||||
|
border-left: none;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-chart-content {
|
||||||
|
padding: 6px 14px 12px;
|
||||||
|
border: 1px solid var(--border-color-base);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f5f8fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-multi-options {
|
&-multi-options {
|
||||||
@@ -39,10 +160,10 @@
|
|||||||
|
|
||||||
&-tip {
|
&-tip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
row-gap: 6px;
|
row-gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
color: var(--text-color);
|
color: var(--text-color-third);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tip-content {
|
&-tip-content {
|
||||||
@@ -51,7 +172,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
row-gap: 6px;
|
row-gap: 6px;
|
||||||
column-gap: 12px;
|
column-gap: 12px;
|
||||||
color: var(--text-color);
|
color: var(--text-color-third);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tip-content-option {
|
&-tip-content-option {
|
||||||
@@ -86,6 +207,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-tip-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-item-filter-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&-mode-name {
|
&-mode-name {
|
||||||
margin-right: -10px;
|
margin-right: -10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -99,27 +230,6 @@
|
|||||||
&-tip-item-option {
|
&-tip-item-option {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-avatar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 40px;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
margin-right: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--chat-blue);
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content {
|
|
||||||
width: calc(100% - 50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content-mobile {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-metric-info-list {
|
&-metric-info-list {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const BarChart: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilterSection = dimensionFilters?.length > 0;
|
// const hasFilterSection = dimensionFilters?.length > 0;
|
||||||
|
|
||||||
const prefixCls = `${PREFIX_CLS}-bar`;
|
const prefixCls = `${PREFIX_CLS}-bar`;
|
||||||
|
|
||||||
@@ -167,11 +167,11 @@ const BarChart: React.FC<Props> = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className={`${prefixCls}-top-bar`}>
|
<div className={`${prefixCls}-top-bar`}>
|
||||||
<div className={`${prefixCls}-indicator-name`}>{metricColumn?.name}</div>
|
<div className={`${prefixCls}-indicator-name`}>{metricColumn?.name}</div>
|
||||||
{(hasFilterSection || drillDownDimension) && (
|
{drillDownDimension && (
|
||||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||||
(
|
(
|
||||||
<div className={`${prefixCls}-filter-section`}>
|
<div className={`${prefixCls}-filter-section`}>
|
||||||
<FilterSection chatContext={chatContext} entityInfo={entityInfo} />
|
{/* <FilterSection chatContext={chatContext} entityInfo={entityInfo} /> */}
|
||||||
{drillDownDimension && (
|
{drillDownDimension && (
|
||||||
<div className={`${prefixCls}-filter-item`}>
|
<div className={`${prefixCls}-filter-item`}>
|
||||||
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||||
@@ -183,13 +183,13 @@ const BarChart: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{dateInfo && (
|
{/* {dateInfo && (
|
||||||
<div className={`${prefixCls}-date-range`}>
|
<div className={`${prefixCls}-date-range`}>
|
||||||
{dateInfo.startDate === dateInfo.endDate
|
{dateInfo.startDate === dateInfo.endDate
|
||||||
? dateInfo.startDate
|
? dateInfo.startDate
|
||||||
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className={`${prefixCls}-chart`} ref={chartRef} />
|
<div className={`${prefixCls}-chart`} ref={chartRef} />
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { PREFIX_CLS } from '../../../common/constants';
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
import { formatMetric } from '../../../utils/utils';
|
import { formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||||
import ApplyAuth from '../ApplyAuth';
|
import ApplyAuth from '../ApplyAuth';
|
||||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||||
import PeriodCompareItem from './PeriodCompareItem';
|
import PeriodCompareItem from './PeriodCompareItem';
|
||||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import FilterSection from '../FilterSection';
|
import { SwapOutlined } from '@ant-design/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
@@ -26,7 +27,7 @@ const MetricCard: React.FC<Props> = ({
|
|||||||
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||||
|
|
||||||
const { metricInfos } = aggregateInfo || {};
|
const { metricInfos } = aggregateInfo || {};
|
||||||
const { dateInfo, dimensionFilters } = chatContext || {};
|
const { dateInfo } = chatContext || {};
|
||||||
const { startDate } = dateInfo || {};
|
const { startDate } = dateInfo || {};
|
||||||
|
|
||||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||||
@@ -34,25 +35,31 @@ const MetricCard: React.FC<Props> = ({
|
|||||||
|
|
||||||
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||||
|
|
||||||
|
const matricCardClass = classNames(prefixCls, {
|
||||||
|
[`${PREFIX_CLS}-metric-card-dsl`]: queryMode === 'DSL',
|
||||||
|
});
|
||||||
|
|
||||||
const indicatorClass = classNames(`${prefixCls}-indicator`, {
|
const indicatorClass = classNames(`${prefixCls}-indicator`, {
|
||||||
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasFilterSection = dimensionFilters?.length > 0;
|
const [isNumber, setIsNumber] = useState(false);
|
||||||
|
const handleNumberClick = () => {
|
||||||
|
setIsNumber(!isNumber);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={matricCardClass}>
|
||||||
<div className={`${prefixCls}-top-bar`}>
|
<div className={`${prefixCls}-top-bar`}>
|
||||||
{indicatorColumn?.name ? (
|
{indicatorColumn?.name ? (
|
||||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ height: 32 }} />
|
<div style={{ height: 6 }} />
|
||||||
)}
|
)}
|
||||||
{(hasFilterSection || drillDownDimension) && (
|
{drillDownDimension && (
|
||||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||||
(
|
(
|
||||||
<div className={`${prefixCls}-filter-section`}>
|
<div className={`${prefixCls}-filter-section`}>
|
||||||
<FilterSection chatContext={chatContext} entityInfo={entityInfo} />
|
|
||||||
{drillDownDimension && (
|
{drillDownDimension && (
|
||||||
<div className={`${prefixCls}-filter-item`}>
|
<div className={`${prefixCls}-filter-item`}>
|
||||||
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||||
@@ -70,8 +77,15 @@ const MetricCard: React.FC<Props> = ({
|
|||||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||||
<ApplyAuth model={entityInfo?.modelInfo.name || ''} onApplyAuth={onApplyAuth} />
|
<ApplyAuth model={entityInfo?.modelInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||||
) : (
|
) : (
|
||||||
<div className={`${prefixCls}-indicator-value`}>
|
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
{formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'}
|
<div className={`${prefixCls}-indicator-value`}>
|
||||||
|
{isNumber
|
||||||
|
? formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'
|
||||||
|
: formatNumberWithCN(+queryResults?.[0]?.[indicatorColumnName])}
|
||||||
|
</div>
|
||||||
|
<div className={`${prefixCls}-indicator-switch`}>
|
||||||
|
<SwapOutlined onClick={handleNumberClick} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{metricInfos?.length > 0 && (
|
{metricInfos?.length > 0 && (
|
||||||
@@ -90,7 +104,6 @@ const MetricCard: React.FC<Props> = ({
|
|||||||
dimensionFilters={chatContext.dimensionFilters}
|
dimensionFilters={chatContext.dimensionFilters}
|
||||||
drillDownDimension={drillDownDimension}
|
drillDownDimension={drillDownDimension}
|
||||||
onSelectDimension={onSelectDimension}
|
onSelectDimension={onSelectDimension}
|
||||||
isMetricCard
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
|
|
||||||
.@{metric-card-prefix-cls} {
|
.@{metric-card-prefix-cls} {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 130px;
|
height: 162px;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
|
|
||||||
|
&-dsl {
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
&-top-bar {
|
&-top-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
@@ -73,6 +77,13 @@
|
|||||||
color: var(--chat-blue);
|
color: var(--chat-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-indicator-switch {
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
&-period-compare {
|
&-period-compare {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -108,8 +119,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-drill-down-dimensions {
|
&-drill-down-dimensions {
|
||||||
position: absolute;
|
margin-top: 2px;
|
||||||
bottom: -44px;
|
|
||||||
left: -16;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { PREFIX_CLS } from '../../../common/constants';
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
import { formatMetric } from '../../../utils/utils';
|
import { formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||||
import { AggregateInfoType } from '../../../common/type';
|
import { AggregateInfoType } from '../../../common/type';
|
||||||
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||||
|
import { SwapOutlined } from '@ant-design/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
aggregateInfo: AggregateInfoType;
|
aggregateInfo: AggregateInfoType;
|
||||||
@@ -14,18 +16,34 @@ const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
|||||||
|
|
||||||
const prefixCls = `${PREFIX_CLS}-metric-info`;
|
const prefixCls = `${PREFIX_CLS}-metric-info`;
|
||||||
|
|
||||||
|
const [isNumber, setIsNumber] = useState(false);
|
||||||
|
const handleNumberClick = () => {
|
||||||
|
setIsNumber(!isNumber);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-indicator`}>
|
<div className={`${prefixCls}-indicator`}>
|
||||||
<div className={`${prefixCls}-date`}>{date}</div>
|
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
<div className={`${prefixCls}-indicator-value`}>{formatMetric(value)}</div>
|
<div className={`${prefixCls}-indicator-value`}>
|
||||||
{metricInfos?.length > 0 && (
|
{isNumber ? formatMetric(value) : formatNumberWithCN(+value)}
|
||||||
<div className={`${prefixCls}-period-compare`}>
|
|
||||||
{Object.keys(statistics).map((key: any) => (
|
|
||||||
<PeriodCompareItem title={key} value={metricInfos[0].statistics[key]} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className={`${prefixCls}-indicator-switch`}>
|
||||||
|
<SwapOutlined onClick={handleNumberClick} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${prefixCls}-bottom-section`}>
|
||||||
|
<div className={`${prefixCls}-date`}>
|
||||||
|
最新数据日期:<span className={`${prefixCls}-date-value`}>{date}</span>
|
||||||
|
</div>
|
||||||
|
{metricInfos?.length > 0 && (
|
||||||
|
<div className={`${prefixCls}-period-compare`}>
|
||||||
|
{Object.keys(statistics).map((key: any) => (
|
||||||
|
<PeriodCompareItem title={key} value={metricInfos[0].statistics[key]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { ColumnType } from '../../../common/type';
|
import { ColumnType } from '../../../common/type';
|
||||||
import NoPermissionChart from '../NoPermissionChart';
|
import NoPermissionChart from '../NoPermissionChart';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -201,12 +202,16 @@ const MetricTrendChart: React.FC<Props> = ({
|
|||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
||||||
|
|
||||||
|
const flowTrendChartClass = classNames(`${prefixCls}-flow-trend-chart`, {
|
||||||
|
[`${prefixCls}-flow-trend-chart-single`]: !categoryColumnName,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!metricField.authorized ? (
|
{!metricField.authorized ? (
|
||||||
<NoPermissionChart model={model || ''} onApplyAuth={onApplyAuth} />
|
<NoPermissionChart model={model || ''} onApplyAuth={onApplyAuth} />
|
||||||
) : (
|
) : (
|
||||||
<div className={`${prefixCls}-flow-trend-chart`} ref={chartRef} />
|
<div className={flowTrendChartClass} ref={chartRef} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Spin } from 'antd';
|
|||||||
import Table from '../Table';
|
import Table from '../Table';
|
||||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
import MetricInfo from './MetricInfo';
|
import MetricInfo from './MetricInfo';
|
||||||
import FilterSection from '../FilterSection';
|
import MetricOptions from '../../MetricOptions';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
@@ -25,6 +25,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
||||||
|
|
||||||
const [columns, setColumns] = useState<ColumnType[]>([]);
|
const [columns, setColumns] = useState<ColumnType[]>([]);
|
||||||
|
const [defaultMetricField, setDefaultMetricField] = useState<FieldType>();
|
||||||
const [activeMetricField, setActiveMetricField] = useState<FieldType>();
|
const [activeMetricField, setActiveMetricField] = useState<FieldType>();
|
||||||
const [dataSource, setDataSource] = useState<any[]>([]);
|
const [dataSource, setDataSource] = useState<any[]>([]);
|
||||||
const [currentDateOption, setCurrentDateOption] = useState<number>();
|
const [currentDateOption, setCurrentDateOption] = useState<number>();
|
||||||
@@ -58,7 +59,9 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
})?.value;
|
})?.value;
|
||||||
|
|
||||||
setColumns(queryColumns || []);
|
setColumns(queryColumns || []);
|
||||||
setActiveMetricField(chatContext?.metrics?.[0]);
|
const metricField = chatContext?.metrics?.[0];
|
||||||
|
setDefaultMetricField(metricField);
|
||||||
|
setActiveMetricField(metricField);
|
||||||
setDataSource(queryResults);
|
setDataSource(queryResults);
|
||||||
setCurrentDateOption(initialDateOption);
|
setCurrentDateOption(initialDateOption);
|
||||||
setDimensions(chatContext?.dimensions);
|
setDimensions(chatContext?.dimensions);
|
||||||
@@ -107,7 +110,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSwitchMetric = (metricField: FieldType) => {
|
const onSwitchMetric = (metricField?: FieldType) => {
|
||||||
setActiveMetricField(metricField);
|
setActiveMetricField(metricField);
|
||||||
onLoadData({
|
onLoadData({
|
||||||
dateInfo: {
|
dateInfo: {
|
||||||
@@ -116,7 +119,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
unit: currentDateOption || chatContext.dateInfo.unit,
|
unit: currentDateOption || chatContext.dateInfo.unit,
|
||||||
},
|
},
|
||||||
dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined,
|
dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined,
|
||||||
metrics: [metricField],
|
metrics: [metricField || defaultMetricField],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,44 +142,25 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
const isMultipleMetric = chatContext?.metrics?.length > 1;
|
||||||
|
const existDrillDownDimension = queryMode.includes('METRIC') && !isEntityMode;
|
||||||
|
|
||||||
const hasFilterSection = dimensionFilters?.length > 0;
|
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-charts`}>
|
<div className={`${prefixCls}-charts`}>
|
||||||
<div className={`${prefixCls}-top-bar`}>
|
<div className={`${prefixCls}-top-bar`}>
|
||||||
{chatContext.metrics.length > 0 && (
|
<div
|
||||||
<div className={`${prefixCls}-metric-fields`}>
|
className={`${prefixCls}-metric-fields ${prefixCls}-metric-field-single`}
|
||||||
{chatContext.metrics.slice(0, 5).map((metricField: FieldType) => {
|
key={activeMetricField?.bizName}
|
||||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
>
|
||||||
[`${prefixCls}-metric-field-active`]:
|
{activeMetricField?.name}
|
||||||
activeMetricField?.bizName === metricField.bizName &&
|
</div>
|
||||||
chatContext.metrics.length > 1,
|
{drillDownDimension && (
|
||||||
[`${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-wrapper`}>
|
||||||
(
|
(
|
||||||
<div className={`${prefixCls}-filter-section`}>
|
<div className={`${prefixCls}-filter-section`}>
|
||||||
<FilterSection chatContext={chatContext} />
|
|
||||||
{drillDownDimension && (
|
{drillDownDimension && (
|
||||||
<div className={`${prefixCls}-filter-item`}>
|
<div className={`${prefixCls}-filter-item`}>
|
||||||
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||||
@@ -192,7 +176,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
</div>
|
</div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className={`${prefixCls}-content`}>
|
<div className={`${prefixCls}-content`}>
|
||||||
{aggregateInfoValue?.metricInfos?.length > 0 && (
|
{!isMobile && aggregateInfoValue?.metricInfos?.length > 0 && (
|
||||||
<MetricInfo aggregateInfo={aggregateInfoValue} />
|
<MetricInfo aggregateInfo={aggregateInfoValue} />
|
||||||
)}
|
)}
|
||||||
<div className={`${prefixCls}-date-options`}>
|
<div className={`${prefixCls}-date-options`}>
|
||||||
@@ -236,13 +220,25 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{queryMode.includes('METRIC') && !isEntityMode && (
|
{(isMultipleMetric || existDrillDownDimension) && (
|
||||||
<DrillDownDimensions
|
<div className={`${prefixCls}-bottom-tools`}>
|
||||||
modelId={chatContext.modelId}
|
{isMultipleMetric && (
|
||||||
drillDownDimension={drillDownDimension}
|
<MetricOptions
|
||||||
dimensionFilters={chatContext.dimensionFilters}
|
metrics={chatContext.metrics}
|
||||||
onSelectDimension={onSelectDimension}
|
defaultMetric={defaultMetricField}
|
||||||
/>
|
currentMetric={activeMetricField}
|
||||||
|
onSelectMetric={onSwitchMetric}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{existDrillDownDimension && (
|
||||||
|
<DrillDownDimensions
|
||||||
|
modelId={chatContext.modelId}
|
||||||
|
drillDownDimension={drillDownDimension}
|
||||||
|
dimensionFilters={chatContext.dimensionFilters}
|
||||||
|
onSelectDimension={onSelectDimension}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--text-color-third);
|
color: var(--text-color-third);
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-filter-section {
|
&-filter-section {
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
&-indicator {
|
&-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: baseline;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +84,15 @@
|
|||||||
height: 230px;
|
height: 230px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-flow-trend-chart-single {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
&-charts {
|
&-charts {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 12px;
|
row-gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metric-fields {
|
&-metric-fields {
|
||||||
@@ -123,11 +128,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metric-field-active {
|
|
||||||
color: #fff !important;
|
|
||||||
background-color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-metric-field-single {
|
&-metric-field-single {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -165,6 +165,13 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-bottom-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
&-active-identifier {
|
&-active-identifier {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
@@ -184,14 +191,8 @@
|
|||||||
.@{metric-info-prefix-cls} {
|
.@{metric-info-prefix-cls} {
|
||||||
&-indicator {
|
&-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: baseline;
|
||||||
align-items: flex-start;
|
column-gap: 12px;
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-date {
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-indicator-value {
|
&-indicator-value {
|
||||||
@@ -203,12 +204,33 @@
|
|||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-period-compare {
|
&-bottom-section {
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 20px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-date {
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-date-value {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-indicator-switch {
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-period-compare {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 20px;
|
column-gap: 20px;
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => {
|
|||||||
}
|
}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
dataSource={queryResults}
|
dataSource={queryResults}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%', overflowX: 'auto' }}
|
||||||
// scroll={{ x: 'max-content' }}
|
|
||||||
rowClassName={getRowClassName}
|
rowClassName={getRowClassName}
|
||||||
size={size}
|
size={size}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
@table-prefix-cls: ~'@{supersonic-chat-prefix}-table';
|
@table-prefix-cls: ~'@{supersonic-chat-prefix}-table';
|
||||||
|
|
||||||
.@{table-prefix-cls} {
|
.@{table-prefix-cls} {
|
||||||
margin-top: 16px;
|
margin-top: 6px;
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
&-photo {
|
&-photo {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -68,9 +67,13 @@
|
|||||||
|
|
||||||
.ant-table-tbody {
|
.ant-table-tbody {
|
||||||
.ant-table-cell {
|
.ant-table-cell {
|
||||||
padding: 15px 0;
|
padding: 12px 2px;
|
||||||
color: #333;
|
color: var(--text-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-table-pagination.ant-pagination {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { CLS_PREFIX } from '../../../common/constants';
|
||||||
|
import { MsgDataType } from '../../../common/type';
|
||||||
|
import { isProd } from '../../../utils/utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string | number;
|
||||||
|
data: MsgDataType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_HEIGHT = 800;
|
||||||
|
|
||||||
|
const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||||
|
const [pluginUrl, setPluginUrl] = useState('');
|
||||||
|
const [height, setHeight] = useState(DEFAULT_HEIGHT);
|
||||||
|
|
||||||
|
const prefixCls = `${CLS_PREFIX}-web-page`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
webPage: { url, params },
|
||||||
|
} = data.response || {};
|
||||||
|
|
||||||
|
const handleMessage = useCallback((event: MessageEvent) => {
|
||||||
|
const messageData = event.data;
|
||||||
|
const { type, payload } = messageData;
|
||||||
|
if (type === 'changeMiniProgramContainerSize') {
|
||||||
|
const { msgId, height } = payload;
|
||||||
|
if (`${msgId}` === `${id}`) {
|
||||||
|
setHeight(height);
|
||||||
|
// updateMessageContainerScroll();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (messageData === 'storyResize') {
|
||||||
|
const ifr: any = document.getElementById(`reportIframe_${id}`);
|
||||||
|
const iDoc = ifr.contentDocument || ifr.document || ifr.contentWindow;
|
||||||
|
setTimeout(() => {
|
||||||
|
setHeight(isProd() ? calcPageHeight(iDoc) : DEFAULT_HEIGHT);
|
||||||
|
}, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [handleMessage]);
|
||||||
|
|
||||||
|
function calcPageHeight(doc: any) {
|
||||||
|
const titleAreaEl = doc.getElementById('titleArea');
|
||||||
|
const titleAreaHeight = Math.max(
|
||||||
|
titleAreaEl?.clientHeight || 0,
|
||||||
|
titleAreaEl?.scrollHeight || 0
|
||||||
|
);
|
||||||
|
const dashboardGridEl = doc.getElementsByClassName('dashboardGrid')?.[0];
|
||||||
|
const dashboardGridHeight = Math.max(
|
||||||
|
dashboardGridEl?.clientHeight || 0,
|
||||||
|
dashboardGridEl?.scrollHeight || 0
|
||||||
|
);
|
||||||
|
return Math.max(titleAreaHeight + dashboardGridHeight + 10, DEFAULT_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initData = () => {
|
||||||
|
const heightValue =
|
||||||
|
params?.find((option: any) => option.paramType === 'FORWARD' && option.key === 'height')
|
||||||
|
?.value || DEFAULT_HEIGHT;
|
||||||
|
setHeight(heightValue);
|
||||||
|
let urlValue = url;
|
||||||
|
const valueParams = (params || [])
|
||||||
|
.filter((option: any) => option.paramType !== 'FORWARD')
|
||||||
|
.reduce((result: any, item: any) => {
|
||||||
|
result[item.key] = item.value;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
if (urlValue.includes('?type=dashboard') || urlValue.includes('?type=widget')) {
|
||||||
|
const filterData = encodeURIComponent(
|
||||||
|
JSON.stringify(
|
||||||
|
urlValue.includes('dashboard')
|
||||||
|
? {
|
||||||
|
global: valueParams,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
local: valueParams,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
urlValue = urlValue.replace(
|
||||||
|
'?',
|
||||||
|
`?miniProgram=true&reportName=${name}&filterData=${filterData}&`
|
||||||
|
);
|
||||||
|
urlValue =
|
||||||
|
!isProd() && !urlValue.includes('http') ? `http://s2.tmeoa.com${urlValue}` : urlValue;
|
||||||
|
} else {
|
||||||
|
const params = Object.keys(valueParams || {}).map(key => `${key}=${valueParams[key]}`);
|
||||||
|
if (params.length > 0) {
|
||||||
|
if (url.includes('?')) {
|
||||||
|
urlValue = urlValue.replace('?', `?${params.join('&')}&`);
|
||||||
|
} else {
|
||||||
|
urlValue = `${urlValue}?${params.join('&')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// onReportLoaded(heightValue + 190);
|
||||||
|
setPluginUrl(urlValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <div className={prefixCls} style={{ height }}>
|
||||||
|
<iframe
|
||||||
|
id={`reportIframe_${id}`}
|
||||||
|
name={`reportIframe_${id}`}
|
||||||
|
src={pluginUrl}
|
||||||
|
style={{ width: '100%', height, border: 'none' }}
|
||||||
|
title="reportIframe"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
// </div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebPage;
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { isMobile } from '../../utils/utils';
|
import { isMobile } from '../../utils/utils';
|
||||||
import Bar from './Bar';
|
import Bar from './Bar';
|
||||||
import Message from './Message';
|
|
||||||
import MetricCard from './MetricCard';
|
import MetricCard from './MetricCard';
|
||||||
import MetricTrend from './MetricTrend';
|
import MetricTrend from './MetricTrend';
|
||||||
import Table from './Table';
|
import Table from './Table';
|
||||||
import { ColumnType, DrillDownDimensionType, MsgDataType } from '../../common/type';
|
import { ColumnType, DrillDownDimensionType, MsgDataType } from '../../common/type';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { queryData } from '../../service';
|
import { queryData } from '../../service';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { PREFIX_CLS } from '../../common/constants';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
question: string;
|
question: string;
|
||||||
@@ -17,7 +18,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, triggerResize }) => {
|
const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, triggerResize }) => {
|
||||||
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
const { queryColumns, queryResults, chatContext, queryMode } = data;
|
||||||
|
|
||||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||||
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||||
@@ -25,6 +26,8 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
|||||||
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const prefixCls = `${PREFIX_CLS}-chat-msg`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns(queryColumns);
|
setColumns(queryColumns);
|
||||||
setDataSource(queryResults);
|
setDataSource(queryResults);
|
||||||
@@ -39,9 +42,11 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
|||||||
const categoryField = columns.filter(item => item.showType === 'CATEGORY');
|
const categoryField = columns.filter(item => item.showType === 'CATEGORY');
|
||||||
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
||||||
|
|
||||||
|
const isDslMetricCard =
|
||||||
|
queryMode === 'DSL' && singleData && metricFields.length === 1 && columns.length === 1;
|
||||||
|
|
||||||
const isMetricCard =
|
const isMetricCard =
|
||||||
(queryMode.includes('METRIC') ||
|
(queryMode.includes('METRIC') || isDslMetricCard) &&
|
||||||
(queryMode === 'DSL' && singleData && metricFields.length === 1 && columns.length === 1)) &&
|
|
||||||
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
|
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
|
||||||
|
|
||||||
const isText =
|
const isText =
|
||||||
@@ -51,6 +56,14 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
|||||||
queryMode === 'METRIC_INTERPRET') &&
|
queryMode === 'METRIC_INTERPRET') &&
|
||||||
singleData;
|
singleData;
|
||||||
|
|
||||||
|
const isTable =
|
||||||
|
!isText &&
|
||||||
|
!isMetricCard &&
|
||||||
|
(categoryField.length > 1 ||
|
||||||
|
queryMode === 'ENTITY_DETAIL' ||
|
||||||
|
queryMode === 'ENTITY_DIMENSION' ||
|
||||||
|
(categoryField.length === 1 && metricFields.length === 0));
|
||||||
|
|
||||||
const onLoadData = async (value: any) => {
|
const onLoadData = async (value: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { data } = await queryData({
|
const { data } = await queryData({
|
||||||
@@ -72,47 +85,51 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTextContent = () => {
|
||||||
|
let text = dataSource[0][columns[0].nameEn];
|
||||||
|
let htmlCode: string;
|
||||||
|
const match = text.match(/```html([\s\S]*?)```/);
|
||||||
|
htmlCode = match && match[1].trim();
|
||||||
|
if (htmlCode) {
|
||||||
|
text = text.replace(/```html([\s\S]*?)```/, '');
|
||||||
|
}
|
||||||
|
let scriptCode: string;
|
||||||
|
let scriptSrc: string;
|
||||||
|
if (htmlCode) {
|
||||||
|
scriptSrc = htmlCode.match(/<script src="([\s\S]*?)"><\/script>/)?.[1] || '';
|
||||||
|
scriptCode =
|
||||||
|
htmlCode.match(/<script type="text\/javascript">([\s\S]*?)<\/script>/)?.[1] || '';
|
||||||
|
if (scriptSrc) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = scriptSrc;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
if (scriptCode) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.innerHTML = scriptCode;
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
lineHeight: '24px',
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: '100%',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{htmlCode ? <pre>{text}</pre> : text}
|
||||||
|
{!!htmlCode && <div dangerouslySetInnerHTML={{ __html: htmlCode }} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getMsgContent = () => {
|
const getMsgContent = () => {
|
||||||
if (isText) {
|
if (isText) {
|
||||||
let text = dataSource[0][columns[0].nameEn];
|
return getTextContent();
|
||||||
let htmlCode: string;
|
|
||||||
const match = text.match(/```html([\s\S]*?)```/);
|
|
||||||
htmlCode = match && match[1].trim();
|
|
||||||
if (htmlCode) {
|
|
||||||
text = text.replace(/```html([\s\S]*?)```/, '');
|
|
||||||
}
|
|
||||||
let scriptCode: string;
|
|
||||||
let scriptSrc: string;
|
|
||||||
if (htmlCode) {
|
|
||||||
scriptSrc = htmlCode.match(/<script src="([\s\S]*?)"><\/script>/)?.[1] || '';
|
|
||||||
scriptCode =
|
|
||||||
htmlCode.match(/<script type="text\/javascript">([\s\S]*?)<\/script>/)?.[1] || '';
|
|
||||||
if (scriptSrc) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = scriptSrc;
|
|
||||||
document.body.appendChild(script);
|
|
||||||
}
|
|
||||||
if (scriptCode) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.innerHTML = scriptCode;
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.appendChild(script);
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
lineHeight: '24px',
|
|
||||||
width: 'fit-content',
|
|
||||||
maxWidth: '100%',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{htmlCode ? <pre>{text}</pre> : text}
|
|
||||||
{!!htmlCode && <div dangerouslySetInnerHTML={{ __html: htmlCode }} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isMetricCard) {
|
if (isMetricCard) {
|
||||||
return (
|
return (
|
||||||
@@ -124,12 +141,7 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (isTable) {
|
||||||
categoryField.length > 1 ||
|
|
||||||
queryMode === 'ENTITY_DETAIL' ||
|
|
||||||
queryMode === 'ENTITY_DIMENSION' ||
|
|
||||||
(categoryField.length === 1 && metricFields.length === 0)
|
|
||||||
) {
|
|
||||||
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
||||||
}
|
}
|
||||||
if (dateField && metricFields.length > 0) {
|
if (dateField && metricFields.length > 0) {
|
||||||
@@ -157,33 +169,22 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
|||||||
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
let width = '100%';
|
// let width = '100%';
|
||||||
if (isText) {
|
// if (isText) {
|
||||||
width = 'fit-content';
|
// width = 'fit-content';
|
||||||
} else if (isMetricCard) {
|
// } else if (isMetricCard) {
|
||||||
width = '370px';
|
// width = isDslMetricCard ? '290px' : '370px';
|
||||||
} else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
// } else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||||
if (columns.length === 1) {
|
// if (columns.length === 1) {
|
||||||
width = '600px';
|
// width = '600px';
|
||||||
} else if (columns.length === 2) {
|
// } else if (columns.length === 2) {
|
||||||
width = '1000px';
|
// width = '1000px';
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
const chartMsgClass = classNames({ [prefixCls]: !isTable });
|
||||||
<Message
|
|
||||||
position="left"
|
return <div className={chartMsgClass}>{getMsgContent()}</div>;
|
||||||
chatContext={chatContext}
|
|
||||||
entityInfo={entityInfo}
|
|
||||||
title={question}
|
|
||||||
isMobileMode={isMobileMode}
|
|
||||||
width={width}
|
|
||||||
maxWidth={isText && !isMobile ? '80%' : undefined}
|
|
||||||
queryMode={queryMode}
|
|
||||||
>
|
|
||||||
{getMsgContent()}
|
|
||||||
</Message>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatMsg;
|
export default ChatMsg;
|
||||||
|
|||||||
10
webapp/packages/chat-sdk/src/components/ChatMsg/style.less
Normal file
10
webapp/packages/chat-sdk/src/components/ChatMsg/style.less
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import '../../styles/index.less';
|
||||||
|
|
||||||
|
@chat-msg-prefix-cls: ~'@{supersonic-chat-prefix}-chat-msg';
|
||||||
|
|
||||||
|
.@{chat-msg-prefix-cls} {
|
||||||
|
padding: 6px 14px 12px;
|
||||||
|
border: 1px solid var(--border-color-base);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f5f8fb;
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ const DrillDownDimensions: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={drillDownDimensionsSectionClass}>
|
<div className={drillDownDimensionsSectionClass}>
|
||||||
<div className={`${prefixCls}-title`}>快速维度下钻:</div>
|
<div className={`${prefixCls}-title`}>推荐下钻维度:</div>
|
||||||
<div className={`${prefixCls}-content`}>
|
<div className={`${prefixCls}-content`}>
|
||||||
{defaultDimensions.map((dimension, index) => {
|
{defaultDimensions.map((dimension, index) => {
|
||||||
const itemNameClass = classNames(`${prefixCls}-content-item-name`, {
|
const itemNameClass = classNames(`${prefixCls}-content-item-name`, {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
.@{drill-down-dimensions-prefix-cls} {
|
.@{drill-down-dimensions-prefix-cls} {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
&-section {
|
&-section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createFromIconfontCN } from '@ant-design/icons';
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_imm6kslj5s.js',
|
scriptUrl: '//at.alicdn.com/t/c/font_4120566_qiku6b2kol.js',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { CLS_PREFIX } from '../../common/constants';
|
||||||
|
import { FieldType } from '../../common/type';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { isMobile } from '../../utils/utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
metrics: FieldType[];
|
||||||
|
defaultMetric?: FieldType;
|
||||||
|
currentMetric?: FieldType;
|
||||||
|
isMetricCard?: boolean;
|
||||||
|
onSelectMetric: (metric?: FieldType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MetricOptions: React.FC<Props> = ({
|
||||||
|
metrics,
|
||||||
|
defaultMetric,
|
||||||
|
currentMetric,
|
||||||
|
isMetricCard,
|
||||||
|
onSelectMetric,
|
||||||
|
}) => {
|
||||||
|
const DEFAULT_DIMENSION_COUNT = isMobile ? 2 : 5;
|
||||||
|
const prefixCls = `${CLS_PREFIX}-metric-options`;
|
||||||
|
|
||||||
|
const defaultMetrics = metrics
|
||||||
|
.filter(metric => metric.id !== defaultMetric?.id)
|
||||||
|
.slice(0, DEFAULT_DIMENSION_COUNT);
|
||||||
|
|
||||||
|
const sectionClass = classNames(`${prefixCls}-section`, {
|
||||||
|
[`${prefixCls}-metric-card`]: isMetricCard,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={prefixCls}>
|
||||||
|
<div className={sectionClass}>
|
||||||
|
<div className={`${prefixCls}-title`}>推荐相关指标:</div>
|
||||||
|
<div className={`${prefixCls}-content`}>
|
||||||
|
{defaultMetrics.map((metric, index) => {
|
||||||
|
const itemNameClass = classNames(`${prefixCls}-content-item-name`, {
|
||||||
|
[`${prefixCls}-content-item-active`]: currentMetric?.id === metric.id,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={itemNameClass}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectMetric(currentMetric?.id === metric.id ? defaultMetric : metric);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metric.name}
|
||||||
|
</span>
|
||||||
|
{index !== defaultMetrics.length - 1 && <span>、</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{currentMetric?.id !== defaultMetric?.id && (
|
||||||
|
<div
|
||||||
|
className={`${prefixCls}-cancel-select`}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectMetric(defaultMetric);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetricOptions;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@import '../../styles/index.less';
|
||||||
|
|
||||||
|
@metric-options-prefix-cls: ~'@{supersonic-chat-prefix}-metric-options';
|
||||||
|
|
||||||
|
.@{metric-options-prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&-section {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-metric-card {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content-item-name {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid var(--chat-blue);
|
||||||
|
padding: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content-item-active {
|
||||||
|
color: #fff;
|
||||||
|
border-bottom: none;
|
||||||
|
background-color: var(--chat-blue);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-menu-item-active {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-cancel-select {
|
||||||
|
margin-left: 12px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
border: 1px solid var(--text-color-third);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
border-color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,17 +12,10 @@ type Props = {
|
|||||||
entityId: string | number;
|
entityId: string | number;
|
||||||
modelId: number;
|
modelId: number;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
isMobileMode?: boolean;
|
|
||||||
onSelect: (option: string) => void;
|
onSelect: (option: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RecommendOptions: React.FC<Props> = ({
|
const RecommendOptions: React.FC<Props> = ({ entityId, modelId, modelName, onSelect }) => {
|
||||||
entityId,
|
|
||||||
modelId,
|
|
||||||
modelName,
|
|
||||||
isMobileMode,
|
|
||||||
onSelect,
|
|
||||||
}) => {
|
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -125,7 +118,7 @@ const RecommendOptions: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const recommendOptionsClass = classNames(prefixCls, {
|
const recommendOptionsClass = classNames(prefixCls, {
|
||||||
[`${prefixCls}-mobile-mode`]: isMobileMode,
|
[`${prefixCls}-mobile-mode`]: isMobile,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={recommendOptionsClass}>{getSectionOptions()}</div>;
|
return <div className={recommendOptionsClass}>{getSectionOptions()}</div>;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { isMobile } from '../../utils/utils';
|
import { isMobile } from '../../utils/utils';
|
||||||
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
||||||
import { Button, Popover, message } from 'antd';
|
|
||||||
import { CLS_PREFIX } from '../../common/constants';
|
import { CLS_PREFIX } from '../../common/constants';
|
||||||
import { MsgDataType } from '../../common/type';
|
import { MsgDataType } from '../../common/type';
|
||||||
import RecommendOptions from '../RecommendOptions';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { updateQAFeedback } from '../../service';
|
import { updateQAFeedback } from '../../service';
|
||||||
@@ -12,55 +10,19 @@ type Props = {
|
|||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
scoreValue?: number;
|
scoreValue?: number;
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
isMobileMode?: boolean;
|
|
||||||
onSwitchEntity: (entityId: string) => void;
|
|
||||||
onChangeChart: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tools: React.FC<Props> = ({
|
const Tools: React.FC<Props> = ({ data, scoreValue, isLastMessage }) => {
|
||||||
data,
|
const { queryResults, queryId, chatContext, queryMode } = data || {};
|
||||||
scoreValue,
|
|
||||||
isLastMessage,
|
|
||||||
isMobileMode,
|
|
||||||
onSwitchEntity,
|
|
||||||
onChangeChart,
|
|
||||||
}) => {
|
|
||||||
const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false);
|
|
||||||
const { queryColumns, queryResults, queryId, chatContext, queryMode, entityInfo } = data || {};
|
|
||||||
const [score, setScore] = useState(scoreValue || 0);
|
const [score, setScore] = useState(scoreValue || 0);
|
||||||
|
|
||||||
const { dimensionFilters, elementMatches } = data.chatContext;
|
|
||||||
|
|
||||||
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
|
|
||||||
const entityName = elementMatches?.find((item: any) => item.element?.type === 'ID')?.element
|
|
||||||
?.name;
|
|
||||||
|
|
||||||
const isEntityMode =
|
|
||||||
queryMode === 'ENTITY_LIST_FILTER' && typeof entityId === 'string' && entityName !== undefined;
|
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-tools`;
|
const prefixCls = `${CLS_PREFIX}-tools`;
|
||||||
|
|
||||||
const singleData = queryResults.length === 1;
|
const singleData = queryResults?.length === 1;
|
||||||
const isMetricCard =
|
const isMetricCard =
|
||||||
queryMode.includes('METRIC') &&
|
queryMode.includes('METRIC') &&
|
||||||
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
|
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
|
||||||
|
|
||||||
const noDashboard =
|
|
||||||
(queryColumns?.length === 1 &&
|
|
||||||
queryColumns[0].showType === 'CATEGORY' &&
|
|
||||||
queryResults?.length === 1) ||
|
|
||||||
(!queryMode.includes('METRIC') && !queryMode.includes('ENTITY')) ||
|
|
||||||
isMetricCard ||
|
|
||||||
isEntityMode;
|
|
||||||
|
|
||||||
const changeChart = () => {
|
|
||||||
onChangeChart();
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToDashboard = () => {
|
|
||||||
message.info('正在开发中,敬请期待');
|
|
||||||
};
|
|
||||||
|
|
||||||
const like = () => {
|
const like = () => {
|
||||||
setScore(5);
|
setScore(5);
|
||||||
updateQAFeedback(queryId, 5);
|
updateQAFeedback(queryId, 5);
|
||||||
@@ -71,11 +33,6 @@ const Tools: React.FC<Props> = ({
|
|||||||
updateQAFeedback(queryId, 1);
|
updateQAFeedback(queryId, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchEntity = (option: string) => {
|
|
||||||
setRecommendOptionsOpen(false);
|
|
||||||
onSwitchEntity(option);
|
|
||||||
};
|
|
||||||
|
|
||||||
const likeClass = classNames(`${prefixCls}-like`, {
|
const likeClass = classNames(`${prefixCls}-like`, {
|
||||||
[`${prefixCls}-feedback-active`]: score === 5,
|
[`${prefixCls}-feedback-active`]: score === 5,
|
||||||
});
|
});
|
||||||
@@ -85,51 +42,18 @@ const Tools: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
{isLastMessage && chatContext?.modelId && entityInfo?.entityId && (
|
{!isMobile && isLastMessage && (
|
||||||
<Popover
|
<div className={`${prefixCls}-feedback`}>
|
||||||
content={
|
<div>这个回答正确吗?</div>
|
||||||
<RecommendOptions
|
<LikeOutlined className={likeClass} onClick={like} />
|
||||||
entityId={entityInfo.entityId}
|
<DislikeOutlined
|
||||||
modelId={chatContext.modelId}
|
className={dislikeClass}
|
||||||
modelName={chatContext.modelName}
|
onClick={e => {
|
||||||
isMobileMode={isMobileMode}
|
e.stopPropagation();
|
||||||
onSelect={switchEntity}
|
dislike();
|
||||||
/>
|
}}
|
||||||
}
|
/>
|
||||||
placement={isMobileMode ? 'top' : 'right'}
|
</div>
|
||||||
trigger="click"
|
|
||||||
open={recommendOptionsOpen}
|
|
||||||
onOpenChange={open => setRecommendOptionsOpen(open)}
|
|
||||||
>
|
|
||||||
<Button shape="round">切换其他匹配内容</Button>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
{!isMobile && (
|
|
||||||
<>
|
|
||||||
{queryMode === 'METRIC_FILTER' && (
|
|
||||||
<Button shape="round" onClick={changeChart}>
|
|
||||||
切换图表
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!noDashboard && (
|
|
||||||
<Button shape="round" onClick={addToDashboard}>
|
|
||||||
加入看板
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isLastMessage && !isMetricCard && (
|
|
||||||
<div className={`${prefixCls}-feedback`}>
|
|
||||||
<div>这个回答正确吗?</div>
|
|
||||||
<LikeOutlined className={likeClass} onClick={like} />
|
|
||||||
<DislikeOutlined
|
|
||||||
className={dislikeClass}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
dislike();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const Chat = () => {
|
|||||||
setFollowQuestions(['测试1234测试', '测试1234测试', '测试1234测试']);
|
setFollowQuestions(['测试1234测试', '测试1234测试', '测试1234测试']);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 5: 查信息,6: 智能圈选
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<div className={styles.inputMsg}>
|
<div className={styles.inputMsg}>
|
||||||
@@ -51,17 +53,19 @@ const Chat = () => {
|
|||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.chatItem}>
|
{inputMsg && (
|
||||||
<ChatItem
|
<div className={styles.chatItem}>
|
||||||
msg={msg}
|
<ChatItem
|
||||||
// msgData={data}
|
msg={msg}
|
||||||
agentId={6}
|
// msgData={data}
|
||||||
onMsgDataLoaded={onMsgDataLoaded}
|
agentId={6}
|
||||||
isLastMessage
|
onMsgDataLoaded={onMsgDataLoaded}
|
||||||
isMobileMode
|
isLastMessage
|
||||||
triggerResize={triggerResize}
|
isMobileMode
|
||||||
/>
|
triggerResize={triggerResize}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ const DEFAULT_CHAT_ID = 0;
|
|||||||
|
|
||||||
const prefix = '/api';
|
const prefix = '/api';
|
||||||
|
|
||||||
export function searchRecommend(queryText: string, chatId?: number, modelId?: number) {
|
export function searchRecommend(queryText: string, chatId?: number, modelId?: number, agentId?: number) {
|
||||||
return axios.post<Result<SearchRecommendItem[]>>(`${prefix}/chat/query/search`, {
|
return axios.post<Result<SearchRecommendItem[]>>(`${prefix}/chat/query/search`, {
|
||||||
queryText,
|
queryText,
|
||||||
chatId: chatId || DEFAULT_CHAT_ID,
|
chatId: chatId || DEFAULT_CHAT_ID,
|
||||||
modelId,
|
modelId,
|
||||||
|
agentId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,8 @@ export function chatExecute(queryText: string, chatId: number, parseInfo: ChatC
|
|||||||
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/execute`, {
|
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/execute`, {
|
||||||
queryText,
|
queryText,
|
||||||
chatId: chatId || DEFAULT_CHAT_ID,
|
chatId: chatId || DEFAULT_CHAT_ID,
|
||||||
parseInfo,
|
queryId: parseInfo.queryId,
|
||||||
|
parseId: parseInfo.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
@import './global.less';
|
@import './global.less';
|
||||||
|
|
||||||
|
@import '../components/ChatMsg/style.less';
|
||||||
|
|
||||||
@import '../components/ChatMsg/Bar/style.less';
|
@import '../components/ChatMsg/Bar/style.less';
|
||||||
|
|
||||||
@import '../components/ChatMsg/Table/style.less';
|
@import '../components/ChatMsg/Table/style.less';
|
||||||
@@ -27,3 +29,5 @@
|
|||||||
@import "../components/RecommendOptions/style.less";
|
@import "../components/RecommendOptions/style.less";
|
||||||
|
|
||||||
@import "../components/DrillDownDimensions/style.less";
|
@import "../components/DrillDownDimensions/style.less";
|
||||||
|
|
||||||
|
@import "../components/MetricOptions/style.less";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
--primary: 180deg 4%;
|
--primary: 180deg 4%;
|
||||||
--primary-color: #f87653;
|
--primary-color: #f87653;
|
||||||
--blue: #296df3;
|
--blue: #296df3;
|
||||||
|
--green: #00d59c;
|
||||||
--deep-blue: #446dff;
|
--deep-blue: #446dff;
|
||||||
--chat-blue: #1b4aef;
|
--chat-blue: #1b4aef;
|
||||||
--wy-color: #c20c0c;
|
--wy-color: #c20c0c;
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ export const getFormattedValue = (value: number | string, remainZero?: boolean)
|
|||||||
return `${formattedValue}${unit === NumericUnit.None ? '' : unit}`;
|
return `${formattedValue}${unit === NumericUnit.None ? '' : unit}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatNumberWithCN = (num: number) => {
|
||||||
|
if (isNaN(num)) return '-';
|
||||||
|
if (num >= 10000) {
|
||||||
|
return (num / 10000).toFixed(1) + "万";
|
||||||
|
} else {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const groupByColumn = (data: any[], column: string) => {
|
export const groupByColumn = (data: any[], column: string) => {
|
||||||
return data.reduce((result, item) => {
|
return data.reduce((result, item) => {
|
||||||
const resultData = { ...result };
|
const resultData = { ...result };
|
||||||
@@ -152,9 +161,8 @@ export function getLightenDarkenColor(col, amt) {
|
|||||||
} else {
|
} else {
|
||||||
result = hexToRgbObj(col) || {};
|
result = hexToRgbObj(col) || {};
|
||||||
}
|
}
|
||||||
return `rgba(${result.r + amt},${result.g + amt},${result.b + amt}${
|
return `rgba(${result.r + amt},${result.g + amt},${result.b + amt}${result.a ? `,${result.a}` : ''
|
||||||
result.a ? `,${result.a}` : ''
|
})`;
|
||||||
})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChartLightenColor(col) {
|
export function getChartLightenColor(col) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const Settings: LayoutSettings & {
|
|||||||
colorWeak: false,
|
colorWeak: false,
|
||||||
title: '',
|
title: '',
|
||||||
pwa: false,
|
pwa: false,
|
||||||
iconfontUrl: '//at.alicdn.com/t/c/font_3201979_drwu4z3kkbi.js',
|
iconfontUrl: '//at.alicdn.com/t/c/font_4120566_qiku6b2kol.js',
|
||||||
splitMenus: true,
|
splitMenus: true,
|
||||||
menu: {
|
menu: {
|
||||||
defaultOpenAll: true,
|
defaultOpenAll: true,
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ const ENV_KEY = {
|
|||||||
const { APP_TARGET } = process.env;
|
const { APP_TARGET } = process.env;
|
||||||
|
|
||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
|
{
|
||||||
|
path: '/chat/mobile',
|
||||||
|
name: 'chat',
|
||||||
|
component: './Chat',
|
||||||
|
hideInMenu: true,
|
||||||
|
layout: false,
|
||||||
|
envEnableList: [ENV_KEY.CHAT],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import defaultSettings from '../config/defaultSettings';
|
|||||||
import settings from '../config/themeSettings';
|
import settings from '../config/themeSettings';
|
||||||
import { queryToken } from './services/login';
|
import { queryToken } from './services/login';
|
||||||
import { queryCurrentUser } from './services/user';
|
import { queryCurrentUser } from './services/user';
|
||||||
import { traverseRoutes, deleteUrlQuery } from './utils/utils';
|
import { traverseRoutes, deleteUrlQuery, isMobile } from './utils/utils';
|
||||||
import { publicPath } from '../config/defaultSettings';
|
import { publicPath } from '../config/defaultSettings';
|
||||||
import Copilot from './pages/Copilot';
|
import Copilot from './pages/Copilot';
|
||||||
export { request } from './services/request';
|
export { request } from './services/request';
|
||||||
@@ -160,7 +160,7 @@ export const layout: RunTimeLayoutConfig = (params) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{dom}
|
{dom}
|
||||||
{history.location.pathname !== '/chat' && <Copilot />}
|
{history.location.pathname !== '/chat' && !isMobile && <Copilot />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { isEqual } from 'lodash';
|
|||||||
import { ChatItem } from 'supersonic-chat-sdk';
|
import { ChatItem } from 'supersonic-chat-sdk';
|
||||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||||
import { AgentType, MessageItem, MessageTypeEnum } from './type';
|
import { AgentType, MessageItem, MessageTypeEnum } from './type';
|
||||||
import Plugin from './components/Plugin';
|
|
||||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import { MODEL_MODEL_ENTITY_ID_FILTER_MAP } from './constants';
|
import { MODEL_MODEL_ENTITY_ID_FILTER_MAP } from './constants';
|
||||||
@@ -71,34 +70,6 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
updateMessageContainerScroll();
|
updateMessageContainerScroll();
|
||||||
}, [copilotFullscreen]);
|
}, [copilotFullscreen]);
|
||||||
|
|
||||||
const getFollowQuestions = (index: number) => {
|
|
||||||
const followQuestions: string[] = [];
|
|
||||||
const currentMsg = messageList[index];
|
|
||||||
const currentMsgData = currentMsg.msgData;
|
|
||||||
const msgs = messageList.slice(0, index).reverse();
|
|
||||||
|
|
||||||
for (let i = 0; i < msgs.length; i++) {
|
|
||||||
const msg = msgs[i];
|
|
||||||
const msgModelId = msg.msgData?.chatContext?.modelId;
|
|
||||||
const msgEntityId = msg.msgData?.entityInfo?.entityId;
|
|
||||||
const currentMsgModelId = currentMsgData?.chatContext?.modelId;
|
|
||||||
const currentMsgEntityId = currentMsgData?.entityInfo?.entityId;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.PLUGIN) &&
|
|
||||||
!!currentMsgModelId &&
|
|
||||||
msgModelId === currentMsgModelId &&
|
|
||||||
msgEntityId === currentMsgEntityId &&
|
|
||||||
msg.msg
|
|
||||||
) {
|
|
||||||
followQuestions.push(msg.msg);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return followQuestions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilters = (modelId?: number, entityId?: string) => {
|
const getFilters = (modelId?: number, entityId?: string) => {
|
||||||
if (!modelId || !entityId) {
|
if (!modelId || !entityId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -130,8 +101,6 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
parseOptions,
|
parseOptions,
|
||||||
} = msgItem;
|
} = msgItem;
|
||||||
|
|
||||||
const followQuestions = getFollowQuestions(index);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
||||||
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
||||||
@@ -142,7 +111,7 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
<AgentList
|
<AgentList
|
||||||
currentAgentName={msg!}
|
currentAgentName={msg!}
|
||||||
data={agentList}
|
data={agentList}
|
||||||
copilotFullscreen={copilotFullscreen}
|
copilotFullscreen={copilotFullscreen || !isMobileMode}
|
||||||
onSelectAgent={onSelectAgent}
|
onSelectAgent={onSelectAgent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -159,6 +128,7 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
filter={getFilters(modelId, entityId)}
|
filter={getFilters(modelId, entityId)}
|
||||||
isLastMessage={index === messageList.length - 1}
|
isLastMessage={index === messageList.length - 1}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
|
isHistory={isHistory}
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||||
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
|
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
|
||||||
@@ -176,6 +146,7 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
filter={getFilters(modelId, entityId)}
|
filter={getFilters(modelId, entityId)}
|
||||||
isLastMessage={index === messageList.length - 1}
|
isLastMessage={index === messageList.length - 1}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
|
isHistory={isHistory}
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
parseOptions={parseOptions}
|
parseOptions={parseOptions}
|
||||||
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||||
@@ -184,24 +155,6 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === MessageTypeEnum.PLUGIN && (
|
|
||||||
<>
|
|
||||||
<Plugin
|
|
||||||
id={msgId}
|
|
||||||
followQuestions={followQuestions}
|
|
||||||
data={msgData!}
|
|
||||||
scoreValue={score}
|
|
||||||
msg={msgValue || msg || ''}
|
|
||||||
isHistory={isHistory}
|
|
||||||
isLastMessage={index === messageList.length - 1}
|
|
||||||
isMobileMode={isMobileMode}
|
|
||||||
onReportLoaded={(height: number) => {
|
|
||||||
updateMessageContainerScroll(true, height);
|
|
||||||
}}
|
|
||||||
onCheckMore={onCheckMore}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Message from '../Message';
|
|||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import { queryRecommendQuestions } from '../../service';
|
import { queryRecommendQuestions } from '../../service';
|
||||||
import Typing from '../Typing';
|
import Typing from '../Typing';
|
||||||
|
import { isMobile } from '@/utils/utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelectQuestion: (value: string) => void;
|
onSelectQuestion: (value: string) => void;
|
||||||
@@ -34,7 +35,7 @@ const RecommendQuestions: React.FC<Props> = ({ onSelectQuestion }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.recommendQuestions}>
|
<div className={styles.recommendQuestions}>
|
||||||
<LeftAvatar />
|
{!isMobile && <LeftAvatar />}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Typing />
|
<Typing />
|
||||||
) : questions.length > 0 ? (
|
) : questions.length > 0 ? (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
column-gap: 16px;
|
column-gap: 16px;
|
||||||
row-gap: 20px;
|
row-gap: 20px;
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const Chat: React.FC<Props> = ({
|
|||||||
const [currentConversation, setCurrentConversation] = useState<
|
const [currentConversation, setCurrentConversation] = useState<
|
||||||
ConversationDetailType | undefined
|
ConversationDetailType | undefined
|
||||||
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
|
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
|
||||||
const [conversationCollapsed, setConversationCollapsed] = useState(isCopilotMode);
|
const [conversationCollapsed, setConversationCollapsed] = useState(isMobileMode);
|
||||||
const [models, setModels] = useState<ModelType[]>([]);
|
const [models, setModels] = useState<ModelType[]>([]);
|
||||||
const [currentModel, setCurrentModel] = useState<ModelType>();
|
const [currentModel, setCurrentModel] = useState<ModelType>();
|
||||||
const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
|
const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
|
||||||
@@ -161,10 +161,10 @@ const Chat: React.FC<Props> = ({
|
|||||||
const sendHelloRsp = () => {
|
const sendHelloRsp = () => {
|
||||||
setMessageList([
|
setMessageList([
|
||||||
{
|
{
|
||||||
id: uuid(),
|
// id: uuid(),
|
||||||
type: MessageTypeEnum.RECOMMEND_QUESTIONS,
|
// type: MessageTypeEnum.RECOMMEND_QUESTIONS,
|
||||||
// type: MessageTypeEnum.AGENT_LIST,
|
type: MessageTypeEnum.AGENT_LIST,
|
||||||
// msg: currentAgent?.name || '查信息',
|
msg: currentAgent?.name || '查信息',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -172,10 +172,7 @@ const Chat: React.FC<Props> = ({
|
|||||||
const convertHistoryMsg = (list: HistoryMsgItemType[]) => {
|
const convertHistoryMsg = (list: HistoryMsgItemType[]) => {
|
||||||
return list.map((item: HistoryMsgItemType) => ({
|
return list.map((item: HistoryMsgItemType) => ({
|
||||||
id: item.questionId,
|
id: item.questionId,
|
||||||
type:
|
type: MessageTypeEnum.QUESTION,
|
||||||
item.queryResult?.queryMode === MessageTypeEnum.WEB_PAGE
|
|
||||||
? MessageTypeEnum.PLUGIN
|
|
||||||
: MessageTypeEnum.QUESTION,
|
|
||||||
msg: item.queryText,
|
msg: item.queryText,
|
||||||
msgData: item.queryResult,
|
msgData: item.queryResult,
|
||||||
score: item.score,
|
score: item.score,
|
||||||
@@ -350,7 +347,12 @@ const Chat: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => {
|
const onMsgDataLoaded = (
|
||||||
|
data: MsgDataType,
|
||||||
|
questionId: string | number,
|
||||||
|
question: string,
|
||||||
|
valid: boolean,
|
||||||
|
) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
conversationRef?.current?.updateData();
|
conversationRef?.current?.updateData();
|
||||||
}
|
}
|
||||||
@@ -366,28 +368,15 @@ const Chat: React.FC<Props> = ({
|
|||||||
parseOptions: data.parseOptions,
|
parseOptions: data.parseOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (data.queryMode === 'WEB_PAGE') {
|
const msgs = cloneDeep(messageList);
|
||||||
setMessageList([
|
const msg = msgs.find((item) => item.id === questionId);
|
||||||
...messageList,
|
if (msg) {
|
||||||
{
|
msg.msgData = data;
|
||||||
id: uuid(),
|
const msgList = [...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])];
|
||||||
msg: messageList[messageList.length - 1]?.msg,
|
setMessageList(msgList);
|
||||||
type: MessageTypeEnum.PLUGIN,
|
updateChatFilter(data, msgList);
|
||||||
msgData: data,
|
|
||||||
},
|
|
||||||
...(parseOptionsItem ? [parseOptionsItem] : []),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const msgs = cloneDeep(messageList);
|
|
||||||
const msg = msgs.find((item) => item.id === questionId);
|
|
||||||
if (msg) {
|
|
||||||
msg.msgData = data;
|
|
||||||
setMessageList([...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])]);
|
|
||||||
}
|
|
||||||
updateMessageContainerScroll();
|
|
||||||
}
|
}
|
||||||
|
updateMessageContainerScroll(`${questionId}`);
|
||||||
updateChatFilter(data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCheckMore = (data: MsgDataType) => {
|
const onCheckMore = (data: MsgDataType) => {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 16px 0;
|
margin: 16px 0 0;
|
||||||
row-gap: 8px;
|
row-gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobileMode {
|
||||||
.messageList {
|
.messageList {
|
||||||
padding: 20px 12px 60px !important;
|
padding: 20px 12px 60px !important;
|
||||||
}
|
}
|
||||||
@@ -786,10 +786,5 @@
|
|||||||
.ss-chat-item-typing-bubble {
|
.ss-chat-item-typing-bubble {
|
||||||
padding: 16px !important;
|
padding: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
ss-chat-metric-card-drill-down-dimensions {
|
|
||||||
bottom: -38px !important;
|
|
||||||
left: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user