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

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

View File

@@ -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[];

View File

@@ -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,31 +29,48 @@ 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`}> <>
<div className={`${prefixCls}-title-bar`}>
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
<div className={`${prefixCls}-step-title`}></div>
</div>
<div className={`${prefixCls}-content-container ${prefixCls}-last-node`}>
<Spin spinning={entitySwitchLoading}> <Spin spinning={entitySwitchLoading}>
{data?.queryMode === 'WEB_PAGE' ? (
<WebPage id={queryId!} data={data} />
) : (
<ChatMsg <ChatMsg
question={question} question={question}
data={data} data={data}
@@ -61,17 +78,10 @@ const ExecuteItem: React.FC<Props> = ({
isMobileMode={isMobileMode} isMobileMode={isMobileMode}
triggerResize={triggerResize} triggerResize={triggerResize}
/> />
</Spin>
{!isMetricCard && (
<Tools
data={data}
isLastMessage={isLastMessage}
isMobileMode={isMobileMode}
onSwitchEntity={onSwitchEntity}
onChangeChart={onChangeChart}
/>
)} )}
</Spin>
</div> </div>
</>
); );
}; };

View File

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

View File

@@ -1,9 +1,11 @@
import React, { ReactNode } from 'react'; import 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>
{!isOptions && (entityAlias === '歌曲' || entityAlias === '艺人') ? (
<SwicthEntity
entityName={entityName}
chatContext={parseInfo}
onSwitchEntity={onSwitchEntity}
/>
) : (
<div className={itemValueClass}>{entityName}</div> <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;

View File

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

View File

@@ -7,6 +7,7 @@ import ParseTip from './ParseTip';
import ExecuteItem from './ExecuteItem'; import 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,30 +199,28 @@ 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> </div>
{!isMetricCard && data && (
<Tools data={data} scoreValue={undefined} isLastMessage={isLastMessage} />
)}
</div>
</div>
); );
}; };

View File

@@ -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;
@@ -100,27 +231,6 @@
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;
display: flex; display: flex;

View File

@@ -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>

View File

@@ -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 style={{ display: 'flex', alignItems: 'flex-end' }}>
<div className={`${prefixCls}-indicator-value`}> <div className={`${prefixCls}-indicator-value`}>
{formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'} {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>
)} )}

View File

@@ -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;
} }
} }

View File

@@ -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,11 +16,26 @@ 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`}>
{isNumber ? formatMetric(value) : formatNumberWithCN(+value)}
</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 && ( {metricInfos?.length > 0 && (
<div className={`${prefixCls}-period-compare`}> <div className={`${prefixCls}-period-compare`}>
{Object.keys(statistics).map((key: any) => ( {Object.keys(statistics).map((key: any) => (
@@ -28,6 +45,7 @@ const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
)} )}
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@@ -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>
); );

View File

@@ -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 className={`${prefixCls}-metric-fields`}>
{chatContext.metrics.slice(0, 5).map((metricField: FieldType) => {
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
[`${prefixCls}-metric-field-active`]:
activeMetricField?.bizName === metricField.bizName &&
chatContext.metrics.length > 1,
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
});
return (
<div <div
className={metricFieldClass} className={`${prefixCls}-metric-fields ${prefixCls}-metric-field-single`}
key={metricField.bizName} key={activeMetricField?.bizName}
onClick={() => {
if (chatContext.metrics.length > 1) {
onSwitchMetric(metricField);
}
}}
> >
{metricField.name} {activeMetricField?.name}
</div> </div>
); {drillDownDimension && (
})}
</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,7 +220,17 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
/> />
)} )}
</div> </div>
{queryMode.includes('METRIC') && !isEntityMode && ( {(isMultipleMetric || existDrillDownDimension) && (
<div className={`${prefixCls}-bottom-tools`}>
{isMultipleMetric && (
<MetricOptions
metrics={chatContext.metrics}
defaultMetric={defaultMetricField}
currentMetric={activeMetricField}
onSelectMetric={onSwitchMetric}
/>
)}
{existDrillDownDimension && (
<DrillDownDimensions <DrillDownDimensions
modelId={chatContext.modelId} modelId={chatContext.modelId}
drillDownDimension={drillDownDimension} drillDownDimension={drillDownDimension}
@@ -244,6 +238,8 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
onSelectDimension={onSelectDimension} onSelectDimension={onSelectDimension}
/> />
)} )}
</div>
)}
</Spin> </Spin>
</div> </div>
</div> </div>

View File

@@ -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;
} }

View File

@@ -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}
/> />

View File

@@ -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;
}
} }

View File

@@ -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;

View File

@@ -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,8 +85,7 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
}); });
}; };
const getMsgContent = () => { const getTextContent = () => {
if (isText) {
let text = dataSource[0][columns[0].nameEn]; let text = dataSource[0][columns[0].nameEn];
let htmlCode: string; let htmlCode: string;
const match = text.match(/```html([\s\S]*?)```/); const match = text.match(/```html([\s\S]*?)```/);
@@ -113,6 +125,11 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
{!!htmlCode && <div dangerouslySetInnerHTML={{ __html: htmlCode }} />} {!!htmlCode && <div dangerouslySetInnerHTML={{ __html: htmlCode }} />}
</div> </div>
); );
};
const getMsgContent = () => {
if (isText) {
return getTextContent();
} }
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;

View 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;
}

View File

@@ -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`, {

View File

@@ -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%;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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>;

View File

@@ -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,38 +42,7 @@ const Tools: React.FC<Props> = ({
return ( return (
<div className={prefixCls}> <div className={prefixCls}>
{isLastMessage && chatContext?.modelId && entityInfo?.entityId && ( {!isMobile && isLastMessage && (
<Popover
content={
<RecommendOptions
entityId={entityInfo.entityId}
modelId={chatContext.modelId}
modelName={chatContext.modelName}
isMobileMode={isMobileMode}
onSelect={switchEntity}
/>
}
placement={isMobileMode ? 'top' : 'right'}
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 className={`${prefixCls}-feedback`}>
<div></div> <div></div>
<LikeOutlined className={likeClass} onClick={like} /> <LikeOutlined className={likeClass} onClick={like} />
@@ -129,8 +55,6 @@ const Tools: React.FC<Props> = ({
/> />
</div> </div>
)} )}
</>
)}
</div> </div>
); );
}; };

View File

@@ -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,6 +53,7 @@ const Chat = () => {
onSearch={onSearch} onSearch={onSearch}
/> />
</div> </div>
{inputMsg && (
<div className={styles.chatItem}> <div className={styles.chatItem}>
<ChatItem <ChatItem
msg={msg} msg={msg}
@@ -62,6 +65,7 @@ const Chat = () => {
triggerResize={triggerResize} triggerResize={triggerResize}
/> />
</div> </div>
)}
</div> </div>
); );
}; };

View File

@@ -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
}); });
} }

View File

@@ -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";

View File

@@ -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;

View File

@@ -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,8 +161,7 @@ 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}` : ''
})`; })`;
} }

View File

@@ -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,

View File

@@ -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',

View File

@@ -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 />}
</> </>
); );
}, },

View File

@@ -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>
); );
})} })}

View File

@@ -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 ? (

View File

@@ -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;

View File

@@ -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') {
setMessageList([
...messageList,
{
id: uuid(),
msg: messageList[messageList.length - 1]?.msg,
type: MessageTypeEnum.PLUGIN,
msgData: data,
},
...(parseOptionsItem ? [parseOptionsItem] : []),
]);
} else {
const msgs = cloneDeep(messageList); const msgs = cloneDeep(messageList);
const msg = msgs.find((item) => item.id === questionId); const msg = msgs.find((item) => item.id === questionId);
if (msg) { if (msg) {
msg.msgData = data; msg.msgData = data;
setMessageList([...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])]); const msgList = [...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])];
setMessageList(msgList);
updateChatFilter(data, msgList);
} }
updateMessageContainerScroll(); updateMessageContainerScroll(`${questionId}`);
}
updateChatFilter(data);
}; };
const onCheckMore = (data: MsgDataType) => { const onCheckMore = (data: MsgDataType) => {

View File

@@ -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;
}
} }