add drill down dimensions and metric period compare and modify layout (#22)
* [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) add drill down dimensions and metric period compare and modify layout --------- Co-authored-by: williamhliu <williamhliu@tencent.com>
@@ -191,4 +191,4 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0"
|
"node": ">=14.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,21 @@ export type InstructionResonseType = {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MetricInfoType = {
|
||||||
|
date: string;
|
||||||
|
name: string;
|
||||||
|
statistics: any;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AggregateInfoType = {
|
||||||
|
metricInfos: MetricInfoType[]
|
||||||
|
}
|
||||||
|
|
||||||
export type MsgDataType = {
|
export type MsgDataType = {
|
||||||
id: number;
|
id: number;
|
||||||
question: string;
|
question: string;
|
||||||
|
aggregateInfo: AggregateInfoType;
|
||||||
chatContext: ChatContextType;
|
chatContext: ChatContextType;
|
||||||
entityInfo: EntityInfoType;
|
entityInfo: EntityInfoType;
|
||||||
queryAuthorization: any;
|
queryAuthorization: any;
|
||||||
@@ -90,7 +102,7 @@ export type MsgDataType = {
|
|||||||
queryResults: any[];
|
queryResults: any[];
|
||||||
queryId: number;
|
queryId: number;
|
||||||
queryMode: string;
|
queryMode: string;
|
||||||
queryState: MsgValidTypeEnum;
|
queryState: string;
|
||||||
response: InstructionResonseType;
|
response: InstructionResonseType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,7 +159,7 @@ export type SuggestionDataType = {
|
|||||||
export type HistoryMsgItemType = {
|
export type HistoryMsgItemType = {
|
||||||
questionId: number;
|
questionId: number;
|
||||||
queryText: string;
|
queryText: string;
|
||||||
queryResponse: MsgDataType;
|
queryResult: MsgDataType;
|
||||||
chatId: number;
|
chatId: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
feedback: string;
|
feedback: string;
|
||||||
@@ -158,3 +170,10 @@ export type HistoryType = {
|
|||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
list: HistoryMsgItemType[];
|
list: HistoryMsgItemType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DrillDownDimensionType = {
|
||||||
|
id: number;
|
||||||
|
domain: number;
|
||||||
|
name: string;
|
||||||
|
bizName: string;
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { MsgDataType, MsgValidTypeEnum } from '../../common/type';
|
import { MsgDataType } from '../../common/type';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Typing from './Typing';
|
import Typing from './Typing';
|
||||||
import ChatMsg from '../ChatMsg';
|
import ChatMsg from '../ChatMsg';
|
||||||
import { chatQuery } from '../../service';
|
import { chatQuery } from '../../service';
|
||||||
import { MSG_VALID_TIP, PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
import { PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
||||||
import Text from './Text';
|
import Text from './Text';
|
||||||
import Tools from '../Tools';
|
import Tools from '../Tools';
|
||||||
import SemanticDetail from '../SemanticDetail';
|
|
||||||
import IconFont from '../IconFont';
|
import IconFont from '../IconFont';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -19,8 +18,6 @@ type Props = {
|
|||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
onMsgDataLoaded?: (data: MsgDataType) => void;
|
onMsgDataLoaded?: (data: MsgDataType) => void;
|
||||||
onSelectSuggestion?: (value: string) => void;
|
|
||||||
onUpdateMessageScroll?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatItem: React.FC<Props> = ({
|
const ChatItem: React.FC<Props> = ({
|
||||||
@@ -33,12 +30,9 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
triggerResize,
|
triggerResize,
|
||||||
msgData,
|
msgData,
|
||||||
onMsgDataLoaded,
|
onMsgDataLoaded,
|
||||||
onSelectSuggestion,
|
|
||||||
onUpdateMessageScroll,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<MsgDataType>();
|
const [data, setData] = useState<MsgDataType>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [metricInfoList, setMetricInfoList] = useState<any[]>([]);
|
|
||||||
const [tip, setTip] = useState('');
|
const [tip, setTip] = useState('');
|
||||||
|
|
||||||
const updateData = (res: Result<MsgDataType>) => {
|
const updateData = (res: Result<MsgDataType>) => {
|
||||||
@@ -51,8 +45,8 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
|
const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
|
||||||
if (queryState !== MsgValidTypeEnum.NORMAL && queryState !== MsgValidTypeEnum.EMPTY) {
|
if (queryState !== 'SUCCESS') {
|
||||||
setTip(MSG_VALID_TIP[queryState || MsgValidTypeEnum.INVALID]);
|
setTip(PARSE_ERROR_TIP);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
|
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
|
||||||
@@ -109,12 +103,9 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCheckMetricInfo = (data: any) => {
|
const isMetricCard =
|
||||||
setMetricInfoList([...metricInfoList, data]);
|
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||||
if (onUpdateMessageScroll) {
|
data.queryResults?.length === 1;
|
||||||
onUpdateMessageScroll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
@@ -126,22 +117,9 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
data={data}
|
data={data}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
onCheckMetricInfo={onCheckMetricInfo}
|
|
||||||
/>
|
/>
|
||||||
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
{!isMetricCard && (
|
||||||
{metricInfoList.length > 0 && (
|
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
||||||
<div className={`${prefixCls}-metric-info-list`}>
|
|
||||||
{metricInfoList.map(item => (
|
|
||||||
<SemanticDetail
|
|
||||||
dataSource={item}
|
|
||||||
onDimensionSelect={(value: string) => {
|
|
||||||
if (onSelectSuggestion) {
|
|
||||||
onSelectSuggestion(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
// flex: 1;
|
|
||||||
width: calc(100% - 50px);
|
width: calc(100% - 50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
|
|
||||||
&-typing-bubble {
|
&-typing-bubble {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 8px 16px !important;
|
padding: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-text-bubble {
|
&-text-bubble {
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
import { CHART_BLUE_COLOR, CHART_SECONDARY_COLOR, PREFIX_CLS } from '../../../common/constants';
|
import { CHART_BLUE_COLOR, CHART_SECONDARY_COLOR, PREFIX_CLS } from '../../../common/constants';
|
||||||
import { MsgDataType } from '../../../common/type';
|
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||||
import { getChartLightenColor, getFormattedValue } from '../../../utils/utils';
|
import { getChartLightenColor, getFormattedValue } from '../../../utils/utils';
|
||||||
import type { ECharts } from 'echarts';
|
import type { ECharts } from 'echarts';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import NoPermissionChart from '../NoPermissionChart';
|
import NoPermissionChart from '../NoPermissionChart';
|
||||||
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import FilterSection from '../FilterSection';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
|
drillDownDimension?: DrillDownDimensionType;
|
||||||
|
loading: boolean;
|
||||||
|
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||||
onApplyAuth?: (domain: string) => void;
|
onApplyAuth?: (domain: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
const BarChart: React.FC<Props> = ({
|
||||||
|
data,
|
||||||
|
triggerResize,
|
||||||
|
drillDownDimension,
|
||||||
|
loading,
|
||||||
|
onSelectDimension,
|
||||||
|
onApplyAuth,
|
||||||
|
}) => {
|
||||||
const chartRef = useRef<any>();
|
const chartRef = useRef<any>();
|
||||||
const [instance, setInstance] = useState<ECharts>();
|
const [instance, setInstance] = useState<ECharts>();
|
||||||
|
|
||||||
const { queryColumns, queryResults, entityInfo } = data;
|
const { queryColumns, queryResults, entityInfo, chatContext, queryMode } = data;
|
||||||
|
|
||||||
|
const { dateInfo } = chatContext || {};
|
||||||
|
|
||||||
const categoryColumnName =
|
const categoryColumnName =
|
||||||
queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || '';
|
queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || '';
|
||||||
const metricColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
const metricColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||||
@@ -35,13 +51,13 @@ const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
);
|
);
|
||||||
const xData = data.map(item => item[categoryColumnName]);
|
const xData = data.map(item => item[categoryColumnName]);
|
||||||
instanceObj.setOption({
|
instanceObj.setOption({
|
||||||
legend: {
|
// legend: {
|
||||||
left: 0,
|
// left: 0,
|
||||||
top: 0,
|
// top: 0,
|
||||||
icon: 'rect',
|
// icon: 'rect',
|
||||||
itemWidth: 15,
|
// itemWidth: 15,
|
||||||
itemHeight: 5,
|
// itemHeight: 5,
|
||||||
},
|
// },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
axisTick: {
|
axisTick: {
|
||||||
@@ -99,7 +115,7 @@ const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
left: '2%',
|
left: '2%',
|
||||||
right: '1%',
|
right: '1%',
|
||||||
bottom: '3%',
|
bottom: '3%',
|
||||||
top: 50,
|
top: 20,
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
@@ -150,7 +166,30 @@ const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={`${PREFIX_CLS}-bar`} ref={chartRef} />;
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={`${PREFIX_CLS}-bar-metric-name`}>{metricColumn?.name}</div>
|
||||||
|
<FilterSection chatContext={chatContext} />
|
||||||
|
{dateInfo && (
|
||||||
|
<div className={`${PREFIX_CLS}-bar-date-range`}>
|
||||||
|
{dateInfo.startDate === dateInfo.endDate
|
||||||
|
? dateInfo.startDate
|
||||||
|
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<div className={`${PREFIX_CLS}-bar-chart`} ref={chartRef} />
|
||||||
|
</Spin>
|
||||||
|
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||||
|
<DrillDownDimensions
|
||||||
|
domainId={chatContext.domainId}
|
||||||
|
drillDownDimension={drillDownDimension}
|
||||||
|
dimensionFilters={chatContext.dimensionFilters}
|
||||||
|
onSelectDimension={onSelectDimension}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BarChart;
|
export default BarChart;
|
||||||
|
|||||||
@@ -3,6 +3,20 @@
|
|||||||
@bar-cls: ~'@{supersonic-chat-prefix}-bar';
|
@bar-cls: ~'@{supersonic-chat-prefix}-bar';
|
||||||
|
|
||||||
.@{bar-cls} {
|
.@{bar-cls} {
|
||||||
height: 270px;
|
|
||||||
margin-top: 16px;
|
&-chart {
|
||||||
|
height: 260px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-metric-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-date-range {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
|
import { ChatContextType } from '../../../common/type';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
chatContext?: ChatContextType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterSection: React.FC<Props> = ({ chatContext }) => {
|
||||||
|
const prefixCls = `${PREFIX_CLS}-filter-section`;
|
||||||
|
|
||||||
|
const { dimensionFilters } = chatContext || {};
|
||||||
|
|
||||||
|
const hasFilterSection = dimensionFilters && dimensionFilters.length > 0;
|
||||||
|
|
||||||
|
return hasFilterSection ? (
|
||||||
|
<div className={prefixCls}>
|
||||||
|
<div className={`${prefixCls}-field-label`}>筛选条件:</div>
|
||||||
|
<div className={`${prefixCls}-filter-values`}>
|
||||||
|
{dimensionFilters.map(filterItem => {
|
||||||
|
const filterValue =
|
||||||
|
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${prefixCls}-filter-item`}
|
||||||
|
key={filterItem.name}
|
||||||
|
title={filterValue.join('、')}
|
||||||
|
>
|
||||||
|
<span className={`${prefixCls}-field-name`}>{filterItem.name}:</span>
|
||||||
|
<span className={`${prefixCls}-filter-value`}>{filterValue.join('、')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterSection;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@import '../../../styles/index.less';
|
||||||
|
|
||||||
|
@filter-section-prefix-cls: ~'@{supersonic-chat-prefix}-filter-section';
|
||||||
|
|
||||||
|
.@{filter-section-prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&-filter-values {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item {
|
||||||
|
padding: 2px 12px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
background-color: #edf2f2;
|
||||||
|
border-radius: 13px;
|
||||||
|
max-width: 200px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-value {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,7 +65,14 @@ const Message: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
|
<div className={`${prefixCls}-title-bar`}>
|
||||||
|
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
|
||||||
|
{position === 'left' && leftTitle && (
|
||||||
|
<div className={`${prefixCls}-top-bar`} title={leftTitle}>
|
||||||
|
({leftTitle})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className={`${prefixCls}-content`}>
|
<div className={`${prefixCls}-content`}>
|
||||||
<div className={`${prefixCls}-body`}>
|
<div className={`${prefixCls}-body`}>
|
||||||
<div
|
<div
|
||||||
@@ -75,14 +82,9 @@ const Message: React.FC<Props> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{position === 'left' && title && (
|
{entityInfoList.length > 0 && (
|
||||||
<div className={`${prefixCls}-top-bar`} title={leftTitle}>
|
|
||||||
{leftTitle}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(entityInfoList.length > 0 || hasFilterSection) && (
|
|
||||||
<div className={`${prefixCls}-info-bar`}>
|
<div className={`${prefixCls}-info-bar`}>
|
||||||
{filterSection}
|
{/* {filterSection} */}
|
||||||
{entityInfoList.length > 0 && (
|
{entityInfoList.length > 0 && (
|
||||||
<div className={`${prefixCls}-main-entity-info`}>
|
<div className={`${prefixCls}-main-entity-info`}>
|
||||||
{entityInfoList.slice(0, 4).map(dimension => {
|
{entityInfoList.slice(0, 4).map(dimension => {
|
||||||
|
|||||||
@@ -3,11 +3,28 @@
|
|||||||
@msg-prefix-cls: ~'@{supersonic-chat-prefix}-message';
|
@msg-prefix-cls: ~'@{supersonic-chat-prefix}-message';
|
||||||
|
|
||||||
.@{msg-prefix-cls} {
|
.@{msg-prefix-cls} {
|
||||||
|
&-title-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
column-gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
&-domain-name {
|
&-domain-name {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 2px;
|
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-top-bar {
|
||||||
|
position: relative;
|
||||||
|
max-width: 80%;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
@@ -20,6 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-bubble {
|
&-bubble {
|
||||||
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -30,18 +48,6 @@
|
|||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-top-bar {
|
|
||||||
position: relative;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 4px 0 8px;
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-filter-section {
|
&-filter-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -77,7 +83,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
row-gap: 12px;
|
row-gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 20px;
|
margin-top: 4px;
|
||||||
column-gap: 20px;
|
column-gap: 20px;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
background: rgba(133, 156, 241, 0.1);
|
background: rgba(133, 156, 241, 0.1);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
|
import IconFont from '../../IconFont';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PeriodCompareItem: React.FC<Props> = ({ title, value }) => {
|
||||||
|
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||||
|
|
||||||
|
const itemValueClass = classNames(`${prefixCls}-period-compare-item-value`, {
|
||||||
|
[`${prefixCls}-period-compare-item-value-up`]: !value.includes('-'),
|
||||||
|
[`${prefixCls}-period-compare-item-value-down`]: value.includes('-'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${prefixCls}-period-compare-item`}>
|
||||||
|
<div className={`${prefixCls}-period-compare-item-title`}>{title}</div>
|
||||||
|
<div className={itemValueClass}>
|
||||||
|
<IconFont type={!value.includes('-') ? 'icon-shangsheng' : 'icon-xiajiang'} />
|
||||||
|
<div>{value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PeriodCompareItem;
|
||||||
@@ -1,36 +1,77 @@
|
|||||||
import { PREFIX_CLS } from '../../../common/constants';
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
import { getFormattedValue } from '../../../utils/utils';
|
import { formatByThousandSeperator } from '../../../utils/utils';
|
||||||
import ApplyAuth from '../ApplyAuth';
|
import ApplyAuth from '../ApplyAuth';
|
||||||
import { MsgDataType } from '../../../common/type';
|
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||||
|
import PeriodCompareItem from './PeriodCompareItem';
|
||||||
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
|
drillDownDimension?: DrillDownDimensionType;
|
||||||
|
loading: boolean;
|
||||||
|
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||||
onApplyAuth?: (domain: string) => void;
|
onApplyAuth?: (domain: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetricCard: React.FC<Props> = ({ data, onApplyAuth }) => {
|
const MetricCard: React.FC<Props> = ({
|
||||||
const { queryColumns, queryResults, entityInfo } = data;
|
data,
|
||||||
|
drillDownDimension,
|
||||||
|
loading,
|
||||||
|
onSelectDimension,
|
||||||
|
onApplyAuth,
|
||||||
|
}) => {
|
||||||
|
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||||
|
|
||||||
|
const { metricInfos } = aggregateInfo || {};
|
||||||
|
const { dateInfo } = chatContext || {};
|
||||||
|
const { startDate, endDate } = dateInfo || {};
|
||||||
|
|
||||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||||
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
||||||
|
|
||||||
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||||
|
|
||||||
|
const indicatorClass = classNames(`${prefixCls}-indicator`, {
|
||||||
|
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-indicator`}>
|
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||||
{/* <div className={`${prefixCls}-date-range`}>
|
<Spin spinning={loading}>
|
||||||
{startTime === endTime ? startTime : `${startTime} ~ ${endTime}`}
|
<div className={indicatorClass}>
|
||||||
</div> */}
|
<div className={`${prefixCls}-date-range`}>
|
||||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||||
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
|
|
||||||
) : (
|
|
||||||
<div className={`${prefixCls}-indicator-value`}>
|
|
||||||
{getFormattedValue(queryResults?.[0]?.[indicatorColumnName])}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||||
{/* <div className={`${prefixCls}-indicator-name`}>{query}</div> */}
|
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||||
</div>
|
) : (
|
||||||
|
<div className={`${prefixCls}-indicator-value`}>
|
||||||
|
{formatByThousandSeperator(queryResults?.[0]?.[indicatorColumnName])}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{metricInfos?.length > 0 && (
|
||||||
|
<div className={`${prefixCls}-period-compare`}>
|
||||||
|
{Object.keys(metricInfos[0].statistics).map((key: any) => (
|
||||||
|
<PeriodCompareItem title={key} value={metricInfos[0].statistics[key]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||||
|
<div className={`${prefixCls}-drill-down-dimensions`}>
|
||||||
|
<DrillDownDimensions
|
||||||
|
domainId={chatContext.domainId}
|
||||||
|
dimensionFilters={chatContext.dimensionFilters}
|
||||||
|
drillDownDimension={drillDownDimension}
|
||||||
|
onSelectDimension={onSelectDimension}
|
||||||
|
isMetricCard
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,34 +3,78 @@
|
|||||||
@metric-card-prefix-cls: ~'@{supersonic-chat-prefix}-metric-card';
|
@metric-card-prefix-cls: ~'@{supersonic-chat-prefix}-metric-card';
|
||||||
|
|
||||||
.@{metric-card-prefix-cls} {
|
.@{metric-card-prefix-cls} {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 150px;
|
height: 130px;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
|
|
||||||
|
&-indicator-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&-indicator {
|
&-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-indicator-period-compare {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-date-range {
|
&-date-range {
|
||||||
color: var(--text-color-fourth);
|
color: var(--text-color-fourth);
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-indicator-value {
|
&-indicator-value {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-size: 30px;
|
font-size: 40px;
|
||||||
|
color: var(--chat-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-indicator-name {
|
&-period-compare {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-period-compare-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-period-compare-item-title {
|
||||||
color: var(--text-color-fourth);
|
color: var(--text-color-fourth);
|
||||||
font-size: 14px;
|
}
|
||||||
|
|
||||||
|
&-period-compare-item-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-period-compare-item-value-up {
|
||||||
|
color: rgb(252, 103, 114);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-period-compare-item-value-down {
|
||||||
|
color: rgb(45, 202, 147);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-drill-down-dimensions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -38px;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
|
import { formatByThousandSeperator } from '../../../utils/utils';
|
||||||
|
import { AggregateInfoType } from '../../../common/type';
|
||||||
|
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
aggregateInfo: AggregateInfoType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||||
|
const { metricInfos } = aggregateInfo || {};
|
||||||
|
const metricInfo = metricInfos?.[0] || {};
|
||||||
|
const { date, value, statistics } = metricInfo || {};
|
||||||
|
|
||||||
|
const prefixCls = `${PREFIX_CLS}-metric-info`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={prefixCls}>
|
||||||
|
<div className={`${prefixCls}-indicator`}>
|
||||||
|
<div className={`${prefixCls}-date`}>{date}</div>
|
||||||
|
<div className={`${prefixCls}-indicator-value`}>{formatByThousandSeperator(value)}</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetricInfo;
|
||||||
@@ -1,24 +1,27 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { CLS_PREFIX, DATE_TYPES } from '../../../common/constants';
|
import { CLS_PREFIX, DATE_TYPES } from '../../../common/constants';
|
||||||
import { ColumnType, FieldType, MsgDataType } from '../../../common/type';
|
import { ColumnType, DrillDownDimensionType, FieldType, MsgDataType } from '../../../common/type';
|
||||||
import { isMobile } from '../../../utils/utils';
|
import { isMobile } from '../../../utils/utils';
|
||||||
import { queryData } from '../../../service';
|
import { queryData } from '../../../service';
|
||||||
import MetricTrendChart from './MetricTrendChart';
|
import MetricTrendChart from './MetricTrendChart';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import Table from '../Table';
|
import Table from '../Table';
|
||||||
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
|
import MetricInfo from './MetricInfo';
|
||||||
|
import FilterSection from '../FilterSection';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
onApplyAuth?: (domain: string) => void;
|
onApplyAuth?: (domain: string) => void;
|
||||||
onCheckMetricInfo?: (data: any) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onCheckMetricInfo }) => {
|
const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||||
const { queryColumns, queryResults, entityInfo, chatContext } = data;
|
const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data;
|
||||||
|
|
||||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES[0];
|
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
||||||
const initialDateOption = dateOptions.find(
|
const initialDateOption = dateOptions.find(
|
||||||
(option: any) => option.value === chatContext?.dateInfo?.unit
|
(option: any) => option.value === chatContext?.dateInfo?.unit
|
||||||
)?.value;
|
)?.value;
|
||||||
@@ -29,6 +32,7 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
|||||||
const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
|
const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
|
||||||
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||||
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
|
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
|
||||||
|
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const dateField: any = columns.find(
|
const dateField: any = columns.find(
|
||||||
@@ -57,9 +61,21 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
|||||||
|
|
||||||
const selectDateOption = (dateOption: number) => {
|
const selectDateOption = (dateOption: number) => {
|
||||||
setCurrentDateOption(dateOption);
|
setCurrentDateOption(dateOption);
|
||||||
|
const endDate = moment().subtract(1, 'days').format('YYYY-MM-DD');
|
||||||
|
const startDate = moment(endDate)
|
||||||
|
.subtract(dateOption - 1, 'days')
|
||||||
|
.format('YYYY-MM-DD');
|
||||||
onLoadData({
|
onLoadData({
|
||||||
metrics: [activeMetricField],
|
metrics: [activeMetricField],
|
||||||
dateInfo: { ...chatContext?.dateInfo, unit: dateOption },
|
dimensions: drillDownDimension
|
||||||
|
? [...(chatContext.dimensions || []), drillDownDimension]
|
||||||
|
: undefined,
|
||||||
|
dateInfo: {
|
||||||
|
...chatContext?.dateInfo,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
unit: dateOption,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,10 +83,23 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
|||||||
setActiveMetricField(metricField);
|
setActiveMetricField(metricField);
|
||||||
onLoadData({
|
onLoadData({
|
||||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
||||||
|
dimensions: drillDownDimension
|
||||||
|
? [...(chatContext.dimensions || []), drillDownDimension]
|
||||||
|
: undefined,
|
||||||
metrics: [metricField],
|
metrics: [metricField],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
||||||
|
setDrillDownDimension(dimension);
|
||||||
|
onLoadData({
|
||||||
|
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
||||||
|
metrics: [activeMetricField],
|
||||||
|
dimensions:
|
||||||
|
dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!currentMetricField) {
|
if (!currentMetricField) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -80,6 +109,33 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
|||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-charts`}>
|
<div className={`${prefixCls}-charts`}>
|
||||||
|
{chatContext.metrics.length > 0 && (
|
||||||
|
<div className={`${prefixCls}-metric-fields`}>
|
||||||
|
{chatContext.metrics.map((metricField: FieldType) => {
|
||||||
|
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
||||||
|
[`${prefixCls}-metric-field-active`]:
|
||||||
|
activeMetricField?.bizName === metricField.bizName &&
|
||||||
|
chatContext.metrics.length > 1,
|
||||||
|
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={metricFieldClass}
|
||||||
|
key={metricField.bizName}
|
||||||
|
onClick={() => {
|
||||||
|
if (chatContext.metrics.length > 1) {
|
||||||
|
onSwitchMetric(metricField);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metricField.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{aggregateInfo?.metricInfos?.length > 0 && <MetricInfo aggregateInfo={aggregateInfo} />}
|
||||||
|
<FilterSection chatContext={chatContext} />
|
||||||
<div className={`${prefixCls}-date-options`}>
|
<div className={`${prefixCls}-date-options`}>
|
||||||
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
||||||
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
|
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
|
||||||
@@ -107,41 +163,10 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{chatContext.metrics.length > 0 && (
|
<Spin spinning={loading}>
|
||||||
<div className={`${prefixCls}-metric-fields`}>
|
{dataSource?.length === 1 ? (
|
||||||
{chatContext.metrics.map((metricField: FieldType) => {
|
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
|
||||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
) : (
|
||||||
[`${prefixCls}-metric-field-active`]:
|
|
||||||
activeMetricField?.bizName === metricField.bizName &&
|
|
||||||
chatContext.metrics.length > 1,
|
|
||||||
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={metricFieldClass}
|
|
||||||
key={metricField.bizName}
|
|
||||||
onClick={() => {
|
|
||||||
if (chatContext.metrics.length > 1) {
|
|
||||||
onSwitchMetric(metricField);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <SemanticInfoPopover
|
|
||||||
classId={chatContext.domainId}
|
|
||||||
uniqueId={metricField.bizName}
|
|
||||||
onDetailBtnClick={onCheckMetricInfo}
|
|
||||||
> */}
|
|
||||||
{metricField.name}
|
|
||||||
{/* </SemanticInfoPopover> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dataSource?.length === 1 ? (
|
|
||||||
<Table data={data} onApplyAuth={onApplyAuth} />
|
|
||||||
) : (
|
|
||||||
<Spin spinning={loading}>
|
|
||||||
<MetricTrendChart
|
<MetricTrendChart
|
||||||
domain={entityInfo?.domainInfo.name}
|
domain={entityInfo?.domainInfo.name}
|
||||||
dateColumnName={dateColumnName}
|
dateColumnName={dateColumnName}
|
||||||
@@ -151,7 +176,15 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
|||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
onApplyAuth={onApplyAuth}
|
onApplyAuth={onApplyAuth}
|
||||||
/>
|
/>
|
||||||
</Spin>
|
)}
|
||||||
|
</Spin>
|
||||||
|
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||||
|
<DrillDownDimensions
|
||||||
|
domainId={chatContext.domainId}
|
||||||
|
drillDownDimension={drillDownDimension}
|
||||||
|
dimensionFilters={chatContext.dimensionFilters}
|
||||||
|
onSelectDimension={onSelectDimension}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
@metric-trend-prefix-cls: ~'@{supersonic-chat-prefix}-metric-trend';
|
@metric-trend-prefix-cls: ~'@{supersonic-chat-prefix}-metric-trend';
|
||||||
|
|
||||||
|
@metric-info-prefix-cls: ~'@{supersonic-chat-prefix}-metric-info';
|
||||||
|
|
||||||
.@{metric-trend-prefix-cls} {
|
.@{metric-trend-prefix-cls} {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 16px;
|
margin-top: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
|
|
||||||
@@ -35,14 +37,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-flow-trend-chart {
|
&-flow-trend-chart {
|
||||||
height: 270px;
|
margin-top: 4px;
|
||||||
|
height: 230px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-charts {
|
&-charts {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 16px;
|
row-gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metric-fields {
|
&-metric-fields {
|
||||||
@@ -50,6 +53,8 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
row-gap: 12px;
|
row-gap: 12px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metric-field {
|
&-metric-field {
|
||||||
@@ -85,10 +90,11 @@
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: var(--text-color-secondary);
|
font-size: 15px;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,3 +139,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.@{metric-info-prefix-cls} {
|
||||||
|
&-indicator {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-date {
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-indicator-value {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 40px;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-period-compare {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 20px;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ 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 { MsgDataType } from '../../common/type';
|
import { ColumnType, DrillDownDimensionType, MsgDataType } from '../../common/type';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { queryData } from '../../service';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
question: string;
|
question: string;
|
||||||
@@ -12,7 +14,6 @@ type Props = {
|
|||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
onCheckMetricInfo?: (data: any) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatMsg: React.FC<Props> = ({
|
const ChatMsg: React.FC<Props> = ({
|
||||||
@@ -21,48 +22,95 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
data,
|
data,
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
triggerResize,
|
triggerResize,
|
||||||
onCheckMetricInfo,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
||||||
|
|
||||||
|
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||||
|
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||||
|
|
||||||
|
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
if (!queryColumns || !queryResults) {
|
if (!queryColumns || !queryResults) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const singleData = queryResults.length === 1;
|
const singleData = dataSource.length === 1;
|
||||||
const dateField = queryColumns.find(item => item.showType === 'DATE' || item.type === 'DATE');
|
const dateField = columns.find(item => item.showType === 'DATE' || item.type === 'DATE');
|
||||||
const categoryField = queryColumns.filter(item => item.showType === 'CATEGORY');
|
const categoryField = columns.filter(item => item.showType === 'CATEGORY');
|
||||||
const metricFields = queryColumns.filter(item => item.showType === 'NUMBER');
|
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
||||||
|
|
||||||
|
const isMetricCard =
|
||||||
|
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && singleData;
|
||||||
|
|
||||||
|
const onLoadData = async (value: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
const { data } = await queryData({
|
||||||
|
...chatContext,
|
||||||
|
...value,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
if (data.code === 200) {
|
||||||
|
setColumns(data.data?.queryColumns || []);
|
||||||
|
setDataSource(data.data?.queryResults || []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
||||||
|
setDrillDownDimension(dimension);
|
||||||
|
onLoadData({
|
||||||
|
dimensions:
|
||||||
|
dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getMsgContent = () => {
|
const getMsgContent = () => {
|
||||||
|
if (isMetricCard) {
|
||||||
|
return (
|
||||||
|
<MetricCard
|
||||||
|
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||||
|
loading={loading}
|
||||||
|
drillDownDimension={drillDownDimension}
|
||||||
|
onSelectDimension={onSelectDimension}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
categoryField.length > 1 ||
|
categoryField.length > 1 ||
|
||||||
queryMode === 'ENTITY_DETAIL' ||
|
queryMode === 'ENTITY_DETAIL' ||
|
||||||
queryMode === 'ENTITY_DIMENSION' ||
|
queryMode === 'ENTITY_DIMENSION' ||
|
||||||
(categoryField.length === 1 && metricFields.length === 0)
|
(categoryField.length === 1 && metricFields.length === 0)
|
||||||
) {
|
) {
|
||||||
return <Table data={data} />;
|
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
||||||
}
|
}
|
||||||
if (dateField && metricFields.length > 0) {
|
if (dateField && metricFields.length > 0) {
|
||||||
return (
|
if (!dataSource.every(item => item[dateField.nameEn] === dataSource[0][dateField.nameEn])) {
|
||||||
<MetricTrend
|
return (
|
||||||
data={data}
|
<MetricTrend
|
||||||
triggerResize={triggerResize}
|
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||||
onCheckMetricInfo={onCheckMetricInfo}
|
triggerResize={triggerResize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (singleData) {
|
return (
|
||||||
return <MetricCard data={data} />;
|
<Bar
|
||||||
}
|
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||||
return <Bar data={data} triggerResize={triggerResize} />;
|
triggerResize={triggerResize}
|
||||||
|
loading={loading}
|
||||||
|
drillDownDimension={drillDownDimension}
|
||||||
|
onSelectDimension={onSelectDimension}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let width = '100%';
|
let width = '100%';
|
||||||
if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
if (isMetricCard) {
|
||||||
if (queryColumns.length === 1) {
|
width = '370px';
|
||||||
|
} else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||||
|
if (columns.length === 1) {
|
||||||
width = '600px';
|
width = '600px';
|
||||||
} else if (queryColumns.length === 2) {
|
} else if (columns.length === 2) {
|
||||||
width = '1000px';
|
width = '1000px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { CLS_PREFIX } from '../../common/constants';
|
||||||
|
import { DrillDownDimensionType, FilterItemType } from '../../common/type';
|
||||||
|
import { queryDrillDownDimensions } from '../../service';
|
||||||
|
import { Dropdown } from 'antd';
|
||||||
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
domainId: number;
|
||||||
|
drillDownDimension?: DrillDownDimensionType;
|
||||||
|
isMetricCard?: boolean;
|
||||||
|
dimensionFilters?: FilterItemType[];
|
||||||
|
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DIMENSION_COUNT = 5;
|
||||||
|
const MAX_DIMENSION_COUNT = 20;
|
||||||
|
|
||||||
|
const DrillDownDimensions: React.FC<Props> = ({
|
||||||
|
domainId,
|
||||||
|
drillDownDimension,
|
||||||
|
isMetricCard,
|
||||||
|
dimensionFilters,
|
||||||
|
onSelectDimension,
|
||||||
|
}) => {
|
||||||
|
const [dimensions, setDimensions] = useState<DrillDownDimensionType[]>([]);
|
||||||
|
|
||||||
|
const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`;
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
const res = await queryDrillDownDimensions(domainId);
|
||||||
|
setDimensions(
|
||||||
|
res.data.data.dimensions
|
||||||
|
.filter(dimension => !dimensionFilters?.some(filter => filter.name === dimension.name))
|
||||||
|
.slice(0, MAX_DIMENSION_COUNT)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancelDrillDown = () => {
|
||||||
|
onSelectDimension(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultDimensions = dimensions.slice(0, DEFAULT_DIMENSION_COUNT);
|
||||||
|
|
||||||
|
const drillDownDimensionsSectionClass = classNames(`${prefixCls}-section`, {
|
||||||
|
[`${prefixCls}-metric-card`]: isMetricCard,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={prefixCls}>
|
||||||
|
<div className={drillDownDimensionsSectionClass}>
|
||||||
|
<div className={`${prefixCls}-title`}>快速维度下钻:</div>
|
||||||
|
<div className={`${prefixCls}-content`}>
|
||||||
|
{defaultDimensions.map((dimension, index) => {
|
||||||
|
const itemNameClass = classNames(`${prefixCls}-content-item-name`, {
|
||||||
|
[`${prefixCls}-content-item-active`]: drillDownDimension?.id === dimension.id,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={itemNameClass}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectDimension(
|
||||||
|
drillDownDimension?.id === dimension.id ? undefined : dimension
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dimension.name}
|
||||||
|
</span>
|
||||||
|
{index !== defaultDimensions.length - 1 && <span>、</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{dimensions.length > DEFAULT_DIMENSION_COUNT && (
|
||||||
|
<div>
|
||||||
|
<span>、</span>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: dimensions.slice(DEFAULT_DIMENSION_COUNT).map(dimension => {
|
||||||
|
const itemNameClass = classNames({
|
||||||
|
[`${prefixCls}-menu-item-active`]: drillDownDimension?.id === dimension.id,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
label: (
|
||||||
|
<span
|
||||||
|
className={itemNameClass}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectDimension(dimension);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dimension.name}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
key: dimension.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span className={`${prefixCls}-content-item-name`}>更多</span>
|
||||||
|
<DownOutlined className={`${prefixCls}-down-arrow`} />
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{drillDownDimension && (
|
||||||
|
<div className={`${prefixCls}-cancel-drill-down`} onClick={cancelDrillDown}>
|
||||||
|
取消下钻
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrillDownDimensions;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
@import '../../styles/index.less';
|
||||||
|
|
||||||
|
@drill-down-dimensions-prefix-cls: ~'@{supersonic-chat-prefix}-drill-down-dimensions';
|
||||||
|
|
||||||
|
.@{drill-down-dimensions-prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&-section {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-down-arrow {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-cancel-drill-down {
|
||||||
|
margin-left: 20px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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_hsbqfckf8tl.js',
|
scriptUrl: '//at.alicdn.com/t/c/font_4120566_imm6kslj5s.js',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { CLS_PREFIX } from '../../common/constants';
|
|
||||||
import { Row, Col } from 'antd';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
description: string;
|
|
||||||
basicInfoList: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const BasicInfoSection: React.FC<Props> = ({ description = '', basicInfoList }) => {
|
|
||||||
const prefixCls = `${CLS_PREFIX}-semantic-detail`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={`${prefixCls}-info-bar`}>
|
|
||||||
<div className={`${prefixCls}-main-entity-info`}>
|
|
||||||
{basicInfoList.map(item => {
|
|
||||||
return (
|
|
||||||
<div className={`${prefixCls}-info-item`}>
|
|
||||||
<div className={`${prefixCls}-info-name`}>{item.name}:</div>
|
|
||||||
<div className={`${prefixCls}-info-value`}>{item.value}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{description && (
|
|
||||||
<>
|
|
||||||
<Row>
|
|
||||||
<Col flex="0 0 52px">
|
|
||||||
<div className={`${prefixCls}-description`}> 口径: </div>
|
|
||||||
</Col>
|
|
||||||
<Col flex="1 1 auto">
|
|
||||||
<div className={`${prefixCls}-description`}>{description}</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BasicInfoSection;
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { message, Row, Col } from 'antd';
|
|
||||||
import { isMobile } from '../../utils/utils';
|
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { getRelatedDimensionFromStatInfo } from '../../service';
|
|
||||||
import { CLS_PREFIX } from '../../common/constants';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
classId?: number;
|
|
||||||
uniqueId: string | number;
|
|
||||||
onSelect?: (value: string) => void;
|
|
||||||
};
|
|
||||||
const PAGE_SIZE = isMobile ? 3 : 10;
|
|
||||||
|
|
||||||
const DimensionSection: React.FC<Props> = ({ classId, uniqueId, onSelect }) => {
|
|
||||||
const [dimensions, setDimensions] = useState<string[]>([]);
|
|
||||||
const [dimensionIndex, setDimensionIndex] = useState(0);
|
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-semantic-detail`;
|
|
||||||
|
|
||||||
const queryDimensionList = async () => {
|
|
||||||
const { data: resData } = await getRelatedDimensionFromStatInfo({
|
|
||||||
classId,
|
|
||||||
uniqueId,
|
|
||||||
});
|
|
||||||
const { code, data, msg } = resData;
|
|
||||||
if (code === '0') {
|
|
||||||
setDimensions(
|
|
||||||
data.map(item => {
|
|
||||||
return item.name;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
message.error(msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
queryDimensionList();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reloadDimensionCmds = () => {
|
|
||||||
const dimensionPageCount = Math.ceil(dimensions.length / PAGE_SIZE);
|
|
||||||
setDimensionIndex((dimensionIndex + 1) % dimensionPageCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dimensionList = dimensions.slice(
|
|
||||||
dimensionIndex * PAGE_SIZE,
|
|
||||||
(dimensionIndex + 1) * PAGE_SIZE
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{dimensionList.length > 0 && (
|
|
||||||
<div className={`${prefixCls}-content-section`}>
|
|
||||||
<Row>
|
|
||||||
<Col flex="0 0 80px">
|
|
||||||
<div className={`${prefixCls}-label`}> 常用维度:</div>
|
|
||||||
</Col>
|
|
||||||
<Col flex="1 1" className={`${prefixCls}-content-col`}>
|
|
||||||
<div className={`${prefixCls}-content-col-box`}>
|
|
||||||
{dimensionList.map((dimension, index) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`${prefixCls}-section-item`}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect?.(dimension);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dimension}
|
|
||||||
</span>
|
|
||||||
{index < dimensionList.length - 1 && '、'}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col flex="0 1 50px">
|
|
||||||
<div
|
|
||||||
className={`${prefixCls}-reload`}
|
|
||||||
onClick={() => {
|
|
||||||
reloadDimensionCmds();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReloadOutlined className={`${prefixCls}-reload-icon`} />
|
|
||||||
{!isMobile && <div className={`${prefixCls}-reload-label`}>换一批</div>}
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DimensionSection;
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { getMetricQueryInfo } from '../../service';
|
|
||||||
import { message, Row, Col } from 'antd';
|
|
||||||
import { CLS_PREFIX } from '../../common/constants';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
classId: number;
|
|
||||||
metricName: string;
|
|
||||||
onSelect?: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RecommendQuestions: React.FC<Props> = ({ classId, metricName, onSelect }) => {
|
|
||||||
const [moreMode, setMoreMode] = useState<boolean>(false);
|
|
||||||
const [questionData, setQuestionData] = useState<any[]>([]);
|
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-semantic-detail`;
|
|
||||||
|
|
||||||
const queryMetricQueryInfo = async () => {
|
|
||||||
const { data: resData } = await getMetricQueryInfo({
|
|
||||||
classId,
|
|
||||||
metricName,
|
|
||||||
});
|
|
||||||
const { code, data, msg } = resData;
|
|
||||||
if (code === '0') {
|
|
||||||
setQuestionData(data);
|
|
||||||
} else {
|
|
||||||
message.error(msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
queryMetricQueryInfo();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${prefixCls}-recommend-questions`}>
|
|
||||||
<div className={`${prefixCls}-header`}>
|
|
||||||
<Row>
|
|
||||||
<Col flex="0 0 85px">
|
|
||||||
<div className={`${prefixCls}-label`}> 大家都在问: </div>
|
|
||||||
</Col>
|
|
||||||
<Col flex="1 1" className={`${prefixCls}-content-col`}>
|
|
||||||
{!moreMode && (
|
|
||||||
<div className={`${prefixCls}-content-col-box`}>
|
|
||||||
{questionData.slice(0, 5).map((item, index) => {
|
|
||||||
const { question } = item;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{index !== 0 && '、'}
|
|
||||||
<span
|
|
||||||
key={question}
|
|
||||||
className={`${prefixCls}-question`}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect?.(question);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: question }} />
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
<Col flex="0 1 30px">
|
|
||||||
{!moreMode ? (
|
|
||||||
<span
|
|
||||||
onClick={() => {
|
|
||||||
setMoreMode(true);
|
|
||||||
}}
|
|
||||||
className={`${prefixCls}-more`}
|
|
||||||
>
|
|
||||||
更多
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`${prefixCls}-more`}
|
|
||||||
onClick={() => {
|
|
||||||
setMoreMode(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
收起
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
{moreMode && (
|
|
||||||
<div className={`${prefixCls}-recommend-questions-content`}>
|
|
||||||
<div className={`${prefixCls}-questions`}>
|
|
||||||
{questionData.map(item => {
|
|
||||||
const { question } = item;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={question}
|
|
||||||
className={`${prefixCls}-question`}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect?.(question);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: question }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecommendQuestions;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import Message from '../ChatMsg/Message';
|
|
||||||
import { Space, Row, Col, Divider } from 'antd';
|
|
||||||
import BasicInfoSection from './BasicInfoSection';
|
|
||||||
import DimensionSection from './DimensionSection';
|
|
||||||
import RecommendSection from './RecommendSection';
|
|
||||||
import SemanticTypeTag from '../ChatMsg/SemanticInfoPopover/SemanticTypeTag';
|
|
||||||
import { CLS_PREFIX } from '../../common/constants';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
dataSource?: any;
|
|
||||||
onDimensionSelect?: (value: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SemanticDetail: React.FC<Props> = ({ dataSource, onDimensionSelect }) => {
|
|
||||||
const { name, nameEn, createdBy, description, className, classId, semanticInfoType } = dataSource;
|
|
||||||
|
|
||||||
const semanticDetailCls = `${CLS_PREFIX}-semantic-detail`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Message position="left" width="100%">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Col flex="1">
|
|
||||||
<Space size={20}>
|
|
||||||
<span className={`${semanticDetailCls}-title`}>{`指标详情: ${name}`}</span>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
<Col flex="0 1 40px">
|
|
||||||
<SemanticTypeTag infoType={semanticInfoType} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
<BasicInfoSection
|
|
||||||
description={description}
|
|
||||||
basicInfoList={[
|
|
||||||
{
|
|
||||||
name: '主题域',
|
|
||||||
value: className,
|
|
||||||
},
|
|
||||||
{ name: '创建人', value: createdBy },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Divider style={{ margin: '12px 0 16px 0px' }} />
|
|
||||||
<DimensionSection classId={classId} uniqueId={nameEn} onSelect={onDimensionSelect} />
|
|
||||||
<Divider style={{ margin: '6px 0 12px 0px' }} />
|
|
||||||
<RecommendSection classId={classId} metricName={name} onSelect={onDimensionSelect} />
|
|
||||||
</div>
|
|
||||||
</Message>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SemanticDetail;
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
@import '../../styles/variables.less';
|
|
||||||
|
|
||||||
@semantic-detail-cls: ~'@{supersonic-chat-prefix}-semantic-detail';
|
|
||||||
|
|
||||||
.@{semantic-detail-cls} {
|
|
||||||
&-info-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
column-gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-main-entity-info {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
column-gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info-name {
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info-value {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-left: 15px;
|
|
||||||
line-height: 30px;
|
|
||||||
&::before {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
left: 0;
|
|
||||||
top: 6px;
|
|
||||||
height: 15px;
|
|
||||||
width: 3px;
|
|
||||||
font-size: 0;
|
|
||||||
background: #0e73ff;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid #0e73ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-section-item {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-reload {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
font-size: 12px;
|
|
||||||
column-gap: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
top: 3px;
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-reload-icon {
|
|
||||||
font-size: 10px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-more {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-recommend-questions-content {
|
|
||||||
height: 300px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-question {
|
|
||||||
cursor: pointer;
|
|
||||||
color: rgba(0, 0, 0, 0.87);
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content-col {
|
|
||||||
min-width: 300px;
|
|
||||||
overflow-y: hidden;
|
|
||||||
height: 32px;
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content-col-box{
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ const Chat = () => {
|
|||||||
<div className={styles.chatItem}>
|
<div className={styles.chatItem}>
|
||||||
<ChatItem
|
<ChatItem
|
||||||
msg={msg}
|
msg={msg}
|
||||||
msgData={data}
|
// msgData={data}
|
||||||
onMsgDataLoaded={onMsgDataLoaded}
|
onMsgDataLoaded={onMsgDataLoaded}
|
||||||
followQuestions={followQuestions}
|
followQuestions={followQuestions}
|
||||||
isLastMessage
|
isLastMessage
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ axiosInstance.interceptors.request.use(
|
|||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (token && config?.headers) {
|
if (token && config?.headers) {
|
||||||
config.headers.Auth = `Bearer ${token}`;
|
config.headers.Auth = `Bearer ${token}`;
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import axios from './axiosInstance';
|
import axios from './axiosInstance';
|
||||||
import { ChatContextType, HistoryType, MsgDataType, SearchRecommendItem } from '../common/type';
|
import { ChatContextType, DrillDownDimensionType, HistoryType, MsgDataType, SearchRecommendItem } from '../common/type';
|
||||||
import { QueryDataType } from '../common/type';
|
import { QueryDataType } from '../common/type';
|
||||||
|
|
||||||
const DEFAULT_CHAT_ID = 0;
|
const DEFAULT_CHAT_ID = 999;
|
||||||
|
|
||||||
const prefix = '/api';
|
const prefix = '/api';
|
||||||
|
|
||||||
@@ -74,4 +74,8 @@ export function queryEntities(entityId: string | number, domainId: number) {
|
|||||||
entityId,
|
entityId,
|
||||||
domainId,
|
domainId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function queryDrillDownDimensions(domainId: number) {
|
||||||
|
return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${domainId}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
@import "../components/ChatMsg/SemanticInfoPopover/style.less";
|
@import "../components/ChatMsg/SemanticInfoPopover/style.less";
|
||||||
|
|
||||||
@import "../components/SemanticDetail/style.less";
|
@import "../components/ChatMsg/FilterSection/style.less";
|
||||||
|
|
||||||
@import '../components/ChatItem/style.less';
|
@import '../components/ChatItem/style.less';
|
||||||
|
|
||||||
@@ -28,3 +28,5 @@
|
|||||||
|
|
||||||
@import "../components/Suggestion/style.less";
|
@import "../components/Suggestion/style.less";
|
||||||
|
|
||||||
|
@import "../components/DrillDownDimensions/style.less";
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@
|
|||||||
"@antv/layout": "^0.3.20",
|
"@antv/layout": "^0.3.20",
|
||||||
"@antv/xflow": "^1.0.55",
|
"@antv/xflow": "^1.0.55",
|
||||||
"@babel/runtime": "^7.22.5",
|
"@babel/runtime": "^7.22.5",
|
||||||
"supersonic-chat-sdk": "^0.0.0",
|
|
||||||
"@types/numeral": "^2.0.2",
|
"@types/numeral": "^2.0.2",
|
||||||
"@types/react-draft-wysiwyg": "^1.13.2",
|
"@types/react-draft-wysiwyg": "^1.13.2",
|
||||||
"@types/react-syntax-highlighter": "^13.5.0",
|
"@types/react-syntax-highlighter": "^13.5.0",
|
||||||
@@ -97,6 +96,7 @@
|
|||||||
"react-split-pane": "^2.0.3",
|
"react-split-pane": "^2.0.3",
|
||||||
"react-syntax-highlighter": "^15.4.3",
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
"sql-formatter": "^2.3.3",
|
"sql-formatter": "^2.3.3",
|
||||||
|
"supersonic-chat-sdk": "^0.0.0",
|
||||||
"umi": "^3.2.14",
|
"umi": "^3.2.14",
|
||||||
"umi-request": "^1.0.8"
|
"umi-request": "^1.0.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
|
|||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import { PLACE_HOLDER } from '../constants';
|
import { PLACE_HOLDER } from '../constants';
|
||||||
import { DomainType } from '../type';
|
import { DomainType } from '../type';
|
||||||
|
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inputMsg: string;
|
inputMsg: string;
|
||||||
@@ -17,6 +18,8 @@ type Props = {
|
|||||||
currentDomain?: DomainType;
|
currentDomain?: DomainType;
|
||||||
domains: DomainType[];
|
domains: DomainType[];
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
onToggleCollapseBtn: () => void;
|
||||||
onInputMsgChange: (value: string) => void;
|
onInputMsgChange: (value: string) => void;
|
||||||
onSendMsg: (msg: string, domainId?: number) => void;
|
onSendMsg: (msg: string, domainId?: number) => void;
|
||||||
onAddConversation: () => void;
|
onAddConversation: () => void;
|
||||||
@@ -41,6 +44,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
currentDomain,
|
currentDomain,
|
||||||
domains,
|
domains,
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
|
collapsed,
|
||||||
|
onToggleCollapseBtn,
|
||||||
onInputMsgChange,
|
onInputMsgChange,
|
||||||
onSendMsg,
|
onSendMsg,
|
||||||
onAddConversation,
|
onAddConversation,
|
||||||
@@ -239,6 +244,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
return (
|
return (
|
||||||
<div className={chatFooterClass}>
|
<div className={chatFooterClass}>
|
||||||
<div className={styles.composer}>
|
<div className={styles.composer}>
|
||||||
|
<div className={styles.collapseBtn} onClick={onToggleCollapseBtn}>
|
||||||
|
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
|
</div>
|
||||||
<Tooltip title="新建对话">
|
<Tooltip title="新建对话">
|
||||||
<IconFont
|
<IconFont
|
||||||
type="icon-icon-add-conversation-line"
|
type="icon-icon-add-conversation-line"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Form, Input, Modal } from 'antd';
|
import { Form, Input, Modal } from 'antd';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { updateConversationName } from '../../../service';
|
import { updateConversationName } from '../../service';
|
||||||
import type { ConversationDetailType } from '../../../type';
|
import type { ConversationDetailType } from '../../type';
|
||||||
import { CHAT_TITLE } from '../../../constants';
|
import { CHAT_TITLE } from '../../constants';
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
|
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import IconFont from '@/components/IconFont';
|
||||||
|
import { Dropdown, Input, Menu } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
ForwardRefRenderFunction,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react';
|
||||||
|
import { useLocation } from 'umi';
|
||||||
|
import ConversationModal from './ConversationModal';
|
||||||
|
import { deleteConversation, getAllConversations, saveConversation } from '../service';
|
||||||
|
import styles from './style.less';
|
||||||
|
import { ConversationDetailType } from '../type';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { DEFAULT_CONVERSATION_NAME } from '@/common/constants';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentConversation?: ConversationDetailType;
|
||||||
|
collapsed?: boolean;
|
||||||
|
onSelectConversation: (
|
||||||
|
conversation: ConversationDetailType,
|
||||||
|
name?: string,
|
||||||
|
domainId?: number,
|
||||||
|
entityId?: string,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||||
|
{ currentConversation, collapsed, onSelectConversation },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { q, cid, domainId, entityId } = (location as any).query;
|
||||||
|
const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
|
||||||
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
|
const [editConversation, setEditConversation] = useState<ConversationDetailType>();
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
updateData,
|
||||||
|
onAddConversation,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateData = async () => {
|
||||||
|
const { data } = await getAllConversations();
|
||||||
|
const conversationList = data || [];
|
||||||
|
setConversations(conversationList);
|
||||||
|
return conversationList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
const data = await updateData();
|
||||||
|
if (data.length > 0) {
|
||||||
|
const chatId = localStorage.getItem('CONVERSATION_ID') || cid;
|
||||||
|
if (chatId) {
|
||||||
|
const conversation = data.find((item: any) => item.chatId === +chatId);
|
||||||
|
if (conversation) {
|
||||||
|
onSelectConversation(conversation);
|
||||||
|
} else {
|
||||||
|
onSelectConversation(data[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onSelectConversation(data[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onAddConversation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (q && cid === undefined && window.location.href.includes('/workbench/chat')) {
|
||||||
|
onAddConversation({ name: q, domainId: domainId ? +domainId : undefined, entityId });
|
||||||
|
} else {
|
||||||
|
initData();
|
||||||
|
}
|
||||||
|
}, [q]);
|
||||||
|
|
||||||
|
const addConversation = async (name?: string) => {
|
||||||
|
await saveConversation(name || DEFAULT_CONVERSATION_NAME);
|
||||||
|
return updateData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteConversation = async (id: number) => {
|
||||||
|
await deleteConversation(id);
|
||||||
|
initData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddConversation = async ({
|
||||||
|
name,
|
||||||
|
domainId,
|
||||||
|
entityId,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
domainId?: number;
|
||||||
|
entityId?: string;
|
||||||
|
type?: string;
|
||||||
|
} = {}) => {
|
||||||
|
const data = await addConversation(name);
|
||||||
|
onSelectConversation(data[0], type || name, domainId, entityId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOperate = (key: string, conversation: ConversationDetailType) => {
|
||||||
|
if (key === 'editName') {
|
||||||
|
setEditConversation(conversation);
|
||||||
|
setEditModalVisible(true);
|
||||||
|
} else if (key === 'delete') {
|
||||||
|
onDeleteConversation(conversation.chatId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversationClass = classNames(styles.conversation, {
|
||||||
|
[styles.collapsed]: collapsed,
|
||||||
|
});
|
||||||
|
|
||||||
|
const convertTime = (date: string) => {
|
||||||
|
moment.locale('zh-cn');
|
||||||
|
const now = moment();
|
||||||
|
const inputDate = moment(date);
|
||||||
|
const diffMinutes = now.diff(inputDate, 'minutes');
|
||||||
|
if (diffMinutes < 1) {
|
||||||
|
return '刚刚';
|
||||||
|
} else if (inputDate.isSame(now, 'day')) {
|
||||||
|
return inputDate.format('HH:mm');
|
||||||
|
} else if (inputDate.isSame(now.subtract(1, 'day'), 'day')) {
|
||||||
|
return '昨天';
|
||||||
|
}
|
||||||
|
return inputDate.format('MM/DD');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={conversationClass}>
|
||||||
|
<div className={styles.leftSection}>
|
||||||
|
<div className={styles.searchConversation}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索"
|
||||||
|
prefix={<SearchOutlined className={styles.searchIcon} />}
|
||||||
|
className={styles.searchTask}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={onSearchValueChange}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.conversationList}>
|
||||||
|
{conversations
|
||||||
|
.filter(
|
||||||
|
(conversation) =>
|
||||||
|
searchValue === '' ||
|
||||||
|
conversation.chatName.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((item) => {
|
||||||
|
const conversationItemClass = classNames(styles.conversationItem, {
|
||||||
|
[styles.activeConversationItem]: currentConversation?.chatId === item.chatId,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
key={item.chatId}
|
||||||
|
overlay={
|
||||||
|
<Menu
|
||||||
|
items={[
|
||||||
|
{ label: '修改对话名称', key: 'editName' },
|
||||||
|
{ label: '删除', key: 'delete' },
|
||||||
|
]}
|
||||||
|
onClick={({ key }) => {
|
||||||
|
onOperate(key, item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={conversationItemClass}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectConversation(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconFont type="icon-chat1" className={styles.conversationIcon} />
|
||||||
|
<div className={styles.conversationContent}>
|
||||||
|
<div className={styles.topTitleBar}>
|
||||||
|
<div className={styles.conversationName}>{item.chatName}</div>
|
||||||
|
<div className={styles.conversationTime}>
|
||||||
|
{convertTime(item.lastTime || '')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.subTitle}>{item.lastQuestion}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ConversationModal
|
||||||
|
visible={editModalVisible}
|
||||||
|
editConversation={editConversation}
|
||||||
|
onClose={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
}}
|
||||||
|
onFinish={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
updateData();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default forwardRef(Conversation);
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
.conversation {
|
||||||
|
position: relative;
|
||||||
|
width: 260px;
|
||||||
|
height: 100vh !important;
|
||||||
|
background-color: #fff;
|
||||||
|
border-right: 1px solid var(--border-color-base);
|
||||||
|
|
||||||
|
.leftSection {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.searchConversation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 9px 10px;
|
||||||
|
|
||||||
|
.searchIcon {
|
||||||
|
color: #999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchTask {
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-input {
|
||||||
|
font-size: 13px !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationList {
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
padding: 2px 8px 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
.conversationItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.conversationIcon {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationContent {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.topTitleBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.conversationName {
|
||||||
|
width: 150px;
|
||||||
|
margin-right: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationTime {
|
||||||
|
color: var(--text-color-six);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-color-six);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activeConversationItem {
|
||||||
|
background-color: var(--light-blue-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--light-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateSection {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.operateIcon {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateLabel {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.operateLabel {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-right: 0;
|
||||||
|
|
||||||
|
.leftSection {
|
||||||
|
.searchConversation {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationList {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateSection {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,33 +4,30 @@ 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 { MessageItem, MessageTypeEnum } from './type';
|
import { MessageItem, MessageTypeEnum } from './type';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Skeleton } from 'antd';
|
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
|
import RecommendQuestions from './components/RecommendQuestions';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
chatId: number;
|
chatId: number;
|
||||||
messageList: MessageItem[];
|
messageList: MessageItem[];
|
||||||
miniProgramLoading: boolean;
|
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
|
conversationCollapsed: boolean;
|
||||||
onClickMessageContainer: () => void;
|
onClickMessageContainer: () => void;
|
||||||
onMsgDataLoaded: (data: MsgDataType, questionId: string | number) => void;
|
onMsgDataLoaded: (data: MsgDataType, questionId: string | number) => void;
|
||||||
onSelectSuggestion: (value: string) => void;
|
onSelectSuggestion: (value: string) => void;
|
||||||
onCheckMore: (data: MsgDataType) => void;
|
onCheckMore: (data: MsgDataType) => void;
|
||||||
onUpdateMessageScroll: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageContainer: React.FC<Props> = ({
|
const MessageContainer: React.FC<Props> = ({
|
||||||
id,
|
id,
|
||||||
chatId,
|
chatId,
|
||||||
messageList,
|
messageList,
|
||||||
miniProgramLoading,
|
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
|
conversationCollapsed,
|
||||||
onClickMessageContainer,
|
onClickMessageContainer,
|
||||||
onMsgDataLoaded,
|
onMsgDataLoaded,
|
||||||
onSelectSuggestion,
|
onSelectSuggestion,
|
||||||
onUpdateMessageScroll,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [triggerResize, setTriggerResize] = useState(false);
|
const [triggerResize, setTriggerResize] = useState(false);
|
||||||
|
|
||||||
@@ -41,6 +38,10 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onResize();
|
||||||
|
}, [conversationCollapsed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -48,10 +49,6 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const messageListClass = classNames(styles.messageList, {
|
|
||||||
[styles.miniProgramLoading]: miniProgramLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getFollowQuestions = (index: number) => {
|
const getFollowQuestions = (index: number) => {
|
||||||
const followQuestions: string[] = [];
|
const followQuestions: string[] = [];
|
||||||
const currentMsg = messageList[index];
|
const currentMsg = messageList[index];
|
||||||
@@ -82,8 +79,7 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
||||||
{miniProgramLoading && <Skeleton className={styles.messageLoading} paragraph={{ rows: 5 }} />}
|
<div className={styles.messageList}>
|
||||||
<div className={messageListClass}>
|
|
||||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||||
const { id: msgId, domainId, type, msg, msgValue, identityMsg, msgData } = msgItem;
|
const { id: msgId, domainId, type, msg, msgValue, identityMsg, msgData } = msgItem;
|
||||||
|
|
||||||
@@ -91,6 +87,9 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
||||||
|
{type === MessageTypeEnum.RECOMMEND_QUESTIONS && (
|
||||||
|
<RecommendQuestions onSelectQuestion={onSelectSuggestion} />
|
||||||
|
)}
|
||||||
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
||||||
{type === MessageTypeEnum.QUESTION && (
|
{type === MessageTypeEnum.QUESTION && (
|
||||||
<>
|
<>
|
||||||
@@ -108,8 +107,6 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
onMsgDataLoaded={(data: MsgDataType) => {
|
onMsgDataLoaded={(data: MsgDataType) => {
|
||||||
onMsgDataLoaded(data, msgId);
|
onMsgDataLoaded(data, msgId);
|
||||||
}}
|
}}
|
||||||
onSelectSuggestion={onSelectSuggestion}
|
|
||||||
onUpdateMessageScroll={onUpdateMessageScroll}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -125,7 +122,7 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
|||||||
if (
|
if (
|
||||||
prevProps.id === nextProps.id &&
|
prevProps.id === nextProps.id &&
|
||||||
isEqual(prevProps.messageList, nextProps.messageList) &&
|
isEqual(prevProps.messageList, nextProps.messageList) &&
|
||||||
prevProps.miniProgramLoading === nextProps.miniProgramLoading
|
prevProps.conversationCollapsed === nextProps.conversationCollapsed
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { CloseOutlined } from '@ant-design/icons';
|
|
||||||
import moment from 'moment';
|
|
||||||
import type { ConversationDetailType } from '../../../type';
|
|
||||||
import styles from './style.less';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
conversations: ConversationDetailType[];
|
|
||||||
onSelectConversation: (conversation: ConversationDetailType) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConversationHistory: React.FC<Props> = ({ conversations, onSelectConversation, onClose }) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.conversationHistory}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div className={styles.headerTitle}>历史记录</div>
|
|
||||||
<CloseOutlined className={styles.headerClose} onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.conversationContent}>
|
|
||||||
{conversations.slice(0, 1000).map((conversation) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={conversation.chatId}
|
|
||||||
className={styles.conversationItem}
|
|
||||||
onClick={() => {
|
|
||||||
onSelectConversation(conversation);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.conversationName} title={conversation.chatName}>
|
|
||||||
{conversation.chatName}
|
|
||||||
</div>
|
|
||||||
<div className={styles.conversationTime}>
|
|
||||||
更新时间:{moment(conversation.lastTime).format('YYYY-MM-DD')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConversationHistory;
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
.conversationHistory {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 78px);
|
|
||||||
overflow: hidden;
|
|
||||||
background: #f3f3f7;
|
|
||||||
border-right: 1px solid var(--border-color-base);
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 50px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-bottom: 1px solid #e8e8e8;
|
|
||||||
.headerTitle {
|
|
||||||
color: var(--text-color);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.headerClose {
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.conversationContent {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
.conversationItem {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-bottom: 1px solid var(--border-color-base-bg-5);
|
|
||||||
cursor: pointer;
|
|
||||||
row-gap: 2px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--light-blue-background);
|
|
||||||
}
|
|
||||||
.conversationName {
|
|
||||||
width: 170px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.conversationTime {
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import IconFont from '@/components/IconFont';
|
|
||||||
import { Dropdown, Menu } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
forwardRef,
|
|
||||||
ForwardRefRenderFunction,
|
|
||||||
useImperativeHandle,
|
|
||||||
} from 'react';
|
|
||||||
import { useLocation } from 'umi';
|
|
||||||
import ConversationHistory from './ConversationHistory';
|
|
||||||
import ConversationModal from './ConversationModal';
|
|
||||||
import { deleteConversation, getAllConversations, saveConversation } from '../../service';
|
|
||||||
import styles from './style.less';
|
|
||||||
import { ConversationDetailType } from '../../type';
|
|
||||||
import { DEFAULT_CONVERSATION_NAME } from '../../constants';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
currentConversation?: ConversationDetailType;
|
|
||||||
onSelectConversation: (conversation: ConversationDetailType, name?: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
|
||||||
{ currentConversation, onSelectConversation },
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const location = useLocation();
|
|
||||||
const { q, cid } = (location as any).query;
|
|
||||||
const [originConversations, setOriginConversations] = useState<ConversationDetailType[]>([]);
|
|
||||||
const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
||||||
const [editConversation, setEditConversation] = useState<ConversationDetailType>();
|
|
||||||
const [historyVisible, setHistoryVisible] = useState(false);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
updateData,
|
|
||||||
onAddConversation,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const updateData = async () => {
|
|
||||||
const { data } = await getAllConversations();
|
|
||||||
const conversationList = (data || []).slice(0, 5);
|
|
||||||
setOriginConversations(data || []);
|
|
||||||
setConversations(conversationList);
|
|
||||||
return conversationList;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initData = async () => {
|
|
||||||
const data = await updateData();
|
|
||||||
if (data.length > 0) {
|
|
||||||
const chatId = localStorage.getItem('CONVERSATION_ID') || cid;
|
|
||||||
if (chatId) {
|
|
||||||
const conversation = data.find((item: any) => item.chatId === +chatId);
|
|
||||||
if (conversation) {
|
|
||||||
onSelectConversation(conversation);
|
|
||||||
} else {
|
|
||||||
onSelectConversation(data[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onSelectConversation(data[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onAddConversation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (q && cid === undefined && location.pathname === '/workbench/chat') {
|
|
||||||
onAddConversation(q);
|
|
||||||
} else {
|
|
||||||
initData();
|
|
||||||
}
|
|
||||||
}, [q]);
|
|
||||||
|
|
||||||
const addConversation = async (name?: string) => {
|
|
||||||
await saveConversation(name || DEFAULT_CONVERSATION_NAME);
|
|
||||||
return updateData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeleteConversation = async (id: number) => {
|
|
||||||
await deleteConversation(id);
|
|
||||||
initData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddConversation = async (name?: string) => {
|
|
||||||
const data = await addConversation(name);
|
|
||||||
onSelectConversation(data[0], name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOperate = (key: string, conversation: ConversationDetailType) => {
|
|
||||||
if (key === 'editName') {
|
|
||||||
setEditConversation(conversation);
|
|
||||||
setEditModalVisible(true);
|
|
||||||
} else if (key === 'delete') {
|
|
||||||
onDeleteConversation(conversation.chatId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onShowHistory = () => {
|
|
||||||
setHistoryVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.conversation}>
|
|
||||||
<div className={styles.conversationSection}>
|
|
||||||
<div className={styles.sectionTitle}>对话管理</div>
|
|
||||||
<div className={styles.conversationList}>
|
|
||||||
{conversations.map((item) => {
|
|
||||||
const conversationItemClass = classNames(styles.conversationItem, {
|
|
||||||
[styles.activeConversationItem]: currentConversation?.chatId === item.chatId,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
key={item.chatId}
|
|
||||||
overlay={
|
|
||||||
<Menu
|
|
||||||
items={[
|
|
||||||
{ label: '修改对话名称', key: 'editName' },
|
|
||||||
{ label: '删除', key: 'delete' },
|
|
||||||
]}
|
|
||||||
onClick={({ key }) => {
|
|
||||||
onOperate(key, item);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
trigger={['contextMenu']}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={conversationItemClass}
|
|
||||||
onClick={() => {
|
|
||||||
onSelectConversation(item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.conversationItemContent}>
|
|
||||||
<IconFont type="icon-chat1" className={styles.conversationIcon} />
|
|
||||||
<div className={styles.conversationContent} title={item.chatName}>
|
|
||||||
{item.chatName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className={styles.conversationItem} onClick={onShowHistory}>
|
|
||||||
<div className={styles.conversationItemContent}>
|
|
||||||
<IconFont
|
|
||||||
type="icon-more2"
|
|
||||||
className={`${styles.conversationIcon} ${styles.historyIcon}`}
|
|
||||||
/>
|
|
||||||
<div className={styles.conversationContent}>查看更多对话</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{historyVisible && (
|
|
||||||
<ConversationHistory
|
|
||||||
conversations={originConversations}
|
|
||||||
onSelectConversation={(conversation) => {
|
|
||||||
onSelectConversation(conversation);
|
|
||||||
setHistoryVisible(false);
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setHistoryVisible(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ConversationModal
|
|
||||||
visible={editModalVisible}
|
|
||||||
editConversation={editConversation}
|
|
||||||
onClose={() => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
}}
|
|
||||||
onFinish={() => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
updateData();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default forwardRef(Conversation);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
.conversation {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 0 10px;
|
|
||||||
|
|
||||||
.conversationSection {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.sectionTitle {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversationList {
|
|
||||||
.conversationItem {
|
|
||||||
cursor: pointer;
|
|
||||||
.conversationItemContent {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
color: var(--text-color-third);
|
|
||||||
|
|
||||||
.conversationIcon {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversationContent {
|
|
||||||
width: 160px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-color-third);
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.activeConversationItem,
|
|
||||||
&:hover {
|
|
||||||
.conversationContent {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import type { MsgDataType } from 'supersonic-chat-sdk';
|
|||||||
import Domains from './Domains';
|
import Domains from './Domains';
|
||||||
import { ConversationDetailType, DomainType } from '../type';
|
import { ConversationDetailType, DomainType } from '../type';
|
||||||
import DomainInfo from './Context/DomainInfo';
|
import DomainInfo from './Context/DomainInfo';
|
||||||
import Conversation from './Conversation';
|
import Conversation from '../Conversation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
domains: DomainType[];
|
domains: DomainType[];
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import LeftAvatar from '../LeftAvatar';
|
||||||
|
import Message from '../Message';
|
||||||
|
import styles from './style.less';
|
||||||
|
import { queryRecommendQuestions } from '../../service';
|
||||||
|
import Typing from '../Typing';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSelectQuestion: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecommendQuestions: React.FC<Props> = ({ onSelectQuestion }) => {
|
||||||
|
const [questions, setQuestions] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await queryRecommendQuestions();
|
||||||
|
setLoading(false);
|
||||||
|
setQuestions(
|
||||||
|
res.data?.reduce((result: any[], item: any) => {
|
||||||
|
result = [
|
||||||
|
...result,
|
||||||
|
...item.recommendedQuestions.slice(0, 20).map((item: any) => item.question),
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
}, []) || [],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.recommendQuestions}>
|
||||||
|
<LeftAvatar />
|
||||||
|
{loading ? (
|
||||||
|
<Typing />
|
||||||
|
) : questions.length > 0 ? (
|
||||||
|
<Message position="left" bubbleClassName={styles.recommendQuestionsMsg}>
|
||||||
|
<div className={styles.title}>推荐问题:</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{questions.map((question, index) => (
|
||||||
|
<div
|
||||||
|
key={`${question}_${index}`}
|
||||||
|
className={styles.question}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectQuestion(question);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
|
) : (
|
||||||
|
<Message position="left">您好,请问有什么我可以帮您吗?</Message>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecommendQuestions;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.recommendQuestions {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.recommendQuestionsMsg {
|
||||||
|
padding: 12px 20px 20px !important;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 16px;
|
||||||
|
row-gap: 20px;
|
||||||
|
|
||||||
|
.question {
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-radius:11px;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { querySuggestion } from '../../service';
|
||||||
|
import { SuggestionType } from '../../type';
|
||||||
|
import Message from '../Message';
|
||||||
|
import styles from './style.less';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
domainId: number;
|
||||||
|
onSelectSuggestion: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Suggestion: React.FC<Props> = ({ domainId, onSelectSuggestion }) => {
|
||||||
|
const [data, setData] = useState<SuggestionType>({ dimensions: [], metrics: [] });
|
||||||
|
const { metrics } = data;
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
const res = await querySuggestion(domainId);
|
||||||
|
setData({
|
||||||
|
dimensions: res.data.dimensions.slice(0, 5),
|
||||||
|
metrics: res.data.metrics.slice(0, 5),
|
||||||
|
});
|
||||||
|
updateMessageContainerScroll();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.suggestion}>
|
||||||
|
<Message position="left" bubbleClassName={styles.suggestionMsg}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.rowTitle}>您可能还想问以下指标:</div>
|
||||||
|
<div className={styles.rowContent}>
|
||||||
|
{metrics.map((metric, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={styles.contentItemName}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectSuggestion(metric.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metric.name}
|
||||||
|
</span>
|
||||||
|
{index !== metrics.length - 1 && <span>、</span>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Suggestion;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.suggestion {
|
||||||
|
margin-left: 46px;
|
||||||
|
|
||||||
|
.suggestionMsg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px !important;
|
||||||
|
row-gap: 12px;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 4px;
|
||||||
|
row-gap: 12px;
|
||||||
|
|
||||||
|
.rowTitle {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowContent {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
row-gap: 12px;
|
||||||
|
|
||||||
|
.contentItemName {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid var(--chat-blue);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -229,6 +229,7 @@
|
|||||||
|
|
||||||
.typingBubble {
|
.typingBubble {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
padding: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote {
|
.quote {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-
|
|||||||
import 'supersonic-chat-sdk/dist/index.css';
|
import 'supersonic-chat-sdk/dist/index.css';
|
||||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||||
import { TOKEN_KEY } from '@/services/request';
|
import { TOKEN_KEY } from '@/services/request';
|
||||||
|
import Conversation from './Conversation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isCopilotMode?: boolean;
|
isCopilotMode?: boolean;
|
||||||
@@ -35,6 +36,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
const [miniProgramLoading, setMiniProgramLoading] = useState(false);
|
const [miniProgramLoading, setMiniProgramLoading] = useState(false);
|
||||||
const [domains, setDomains] = useState<DomainType[]>([]);
|
const [domains, setDomains] = useState<DomainType[]>([]);
|
||||||
const [currentDomain, setCurrentDomain] = useState<DomainType>();
|
const [currentDomain, setCurrentDomain] = useState<DomainType>();
|
||||||
|
const [conversationCollapsed, setConversationCollapsed] = useState(false);
|
||||||
const conversationRef = useRef<any>();
|
const conversationRef = useRef<any>();
|
||||||
const chatFooterRef = useRef<any>();
|
const chatFooterRef = useRef<any>();
|
||||||
|
|
||||||
@@ -42,14 +44,14 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
setMessageList([
|
setMessageList([
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: MessageTypeEnum.TEXT,
|
type: MessageTypeEnum.RECOMMEND_QUESTIONS,
|
||||||
msg: '您好,请问有什么我能帮您吗?',
|
// msg: '您好,请问有什么我能帮您吗?',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const existInstuctionMsg = (list: HistoryMsgItemType[]) => {
|
const existInstuctionMsg = (list: HistoryMsgItemType[]) => {
|
||||||
return list.some((msg) => msg.queryResponse.queryMode === MessageTypeEnum.INSTRUCTION);
|
return list.some((msg) => msg.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateScroll = (list: HistoryMsgItemType[]) => {
|
const updateScroll = (list: HistoryMsgItemType[]) => {
|
||||||
@@ -71,11 +73,11 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
...list.map((item: HistoryMsgItemType) => ({
|
...list.map((item: HistoryMsgItemType) => ({
|
||||||
id: item.questionId,
|
id: item.questionId,
|
||||||
type:
|
type:
|
||||||
item.queryResponse?.queryMode === MessageTypeEnum.INSTRUCTION
|
item.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION
|
||||||
? MessageTypeEnum.INSTRUCTION
|
? MessageTypeEnum.INSTRUCTION
|
||||||
: MessageTypeEnum.QUESTION,
|
: MessageTypeEnum.QUESTION,
|
||||||
msg: item.queryText,
|
msg: item.queryText,
|
||||||
msgData: item.queryResponse,
|
msgData: item.queryResult,
|
||||||
isHistory: true,
|
isHistory: true,
|
||||||
})),
|
})),
|
||||||
...(page === 1 ? [] : messageList),
|
...(page === 1 ? [] : messageList),
|
||||||
@@ -85,7 +87,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
sendHelloRsp();
|
sendHelloRsp();
|
||||||
} else {
|
} else {
|
||||||
setCurrentEntity(list[list.length - 1].queryResponse);
|
setCurrentEntity(list[list.length - 1].queryResult);
|
||||||
}
|
}
|
||||||
updateScroll(list);
|
updateScroll(list);
|
||||||
setHistoryInited(true);
|
setHistoryInited(true);
|
||||||
@@ -203,6 +205,10 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
modifyConversationName(currentMsg);
|
modifyConversationName(currentMsg);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onToggleCollapseBtn = () => {
|
||||||
|
setConversationCollapsed(!conversationCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
const onInputMsgChange = (value: string) => {
|
const onInputMsgChange = (value: string) => {
|
||||||
const inputMsgValue = value || '';
|
const inputMsgValue = value || '';
|
||||||
setInputMsg(inputMsgValue);
|
setInputMsg(inputMsgValue);
|
||||||
@@ -295,6 +301,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
const chatClass = classNames(styles.chat, {
|
const chatClass = classNames(styles.chat, {
|
||||||
[styles.mobile]: isMobileMode,
|
[styles.mobile]: isMobileMode,
|
||||||
[styles.copilot]: isCopilotMode,
|
[styles.copilot]: isCopilotMode,
|
||||||
|
[styles.conversationCollapsed]: conversationCollapsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -302,6 +309,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
{!isMobileMode && <Helmet title={WEB_TITLE} />}
|
{!isMobileMode && <Helmet title={WEB_TITLE} />}
|
||||||
<div className={styles.topSection} />
|
<div className={styles.topSection} />
|
||||||
<div className={styles.chatSection}>
|
<div className={styles.chatSection}>
|
||||||
|
<Conversation
|
||||||
|
currentConversation={currentConversation}
|
||||||
|
collapsed={conversationCollapsed}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
ref={conversationRef}
|
||||||
|
/>
|
||||||
<div className={styles.chatApp}>
|
<div className={styles.chatApp}>
|
||||||
{currentConversation && (
|
{currentConversation && (
|
||||||
<div className={styles.chatBody}>
|
<div className={styles.chatBody}>
|
||||||
@@ -310,22 +323,23 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
id="messageContainer"
|
id="messageContainer"
|
||||||
messageList={messageList}
|
messageList={messageList}
|
||||||
chatId={currentConversation?.chatId}
|
chatId={currentConversation?.chatId}
|
||||||
miniProgramLoading={miniProgramLoading}
|
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
|
conversationCollapsed={conversationCollapsed}
|
||||||
onClickMessageContainer={() => {
|
onClickMessageContainer={() => {
|
||||||
inputFocus();
|
inputFocus();
|
||||||
}}
|
}}
|
||||||
onMsgDataLoaded={onMsgDataLoaded}
|
onMsgDataLoaded={onMsgDataLoaded}
|
||||||
onSelectSuggestion={onSendMsg}
|
onSelectSuggestion={onSendMsg}
|
||||||
onCheckMore={onCheckMore}
|
onCheckMore={onCheckMore}
|
||||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
|
||||||
/>
|
/>
|
||||||
<ChatFooter
|
<ChatFooter
|
||||||
inputMsg={inputMsg}
|
inputMsg={inputMsg}
|
||||||
chatId={currentConversation?.chatId}
|
chatId={currentConversation?.chatId}
|
||||||
domains={domains}
|
domains={domains}
|
||||||
currentDomain={currentDomain}
|
currentDomain={currentDomain}
|
||||||
|
collapsed={conversationCollapsed}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
|
onToggleCollapseBtn={onToggleCollapseBtn}
|
||||||
onInputMsgChange={onInputMsgChange}
|
onInputMsgChange={onInputMsgChange}
|
||||||
onSendMsg={(msg: string, domainId?: number) => {
|
onSendMsg={(msg: string, domainId?: number) => {
|
||||||
onSendMsg(msg, messageList, domainId);
|
onSendMsg(msg, messageList, domainId);
|
||||||
@@ -343,7 +357,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isMobileMode && (
|
{/* {!isMobileMode && (
|
||||||
<RightSection
|
<RightSection
|
||||||
domains={domains}
|
domains={domains}
|
||||||
currentEntity={currentEntity}
|
currentEntity={currentEntity}
|
||||||
@@ -353,7 +367,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
onSelectConversation={onSelectConversation}
|
onSelectConversation={onSelectConversation}
|
||||||
conversationRef={conversationRef}
|
conversationRef={conversationRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,3 +30,24 @@ export function getDomainList() {
|
|||||||
skipErrorHandler: true,
|
skipErrorHandler: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateQAFeedback(questionId: number, score: number) {
|
||||||
|
return request<Result<any>>(
|
||||||
|
`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function querySuggestion(domainId: number) {
|
||||||
|
return request<Result<any>>(`${prefix}/chat/recommend/${domainId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryRecommendQuestions() {
|
||||||
|
return request<Result<any>>(`${prefix}/chat/recommend/question`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
.chatApp {
|
.chatApp {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: calc(100vw - 225px);
|
width: calc(100vw - 260px);
|
||||||
height: calc(100vh - 48px);
|
height: calc(100vh - 48px);
|
||||||
padding-left: 20px;
|
padding-left: 10px;
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
|
||||||
.emptyHolder {
|
.emptyHolder {
|
||||||
@@ -230,6 +230,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.conversationCollapsed {
|
||||||
|
.chatApp {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.mobile {
|
&.mobile {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
|
||||||
@@ -243,7 +249,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chatApp {
|
.chatApp {
|
||||||
width: calc(100% - 225px) !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export enum MessageTypeEnum {
|
|||||||
NO_PERMISSION = 'no_permission', // 无权限
|
NO_PERMISSION = 'no_permission', // 无权限
|
||||||
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
|
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
|
||||||
INSTRUCTION = 'INSTRUCTION', // 插件
|
INSTRUCTION = 'INSTRUCTION', // 插件
|
||||||
|
SUGGESTION = 'SUGGESTION',
|
||||||
|
RECOMMEND_QUESTIONS = 'RECOMMEND_QUESTIONS' // 推荐问题
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageItem = {
|
export type MessageItem = {
|
||||||
@@ -41,3 +43,15 @@ export type DomainType = {
|
|||||||
name: string;
|
name: string;
|
||||||
bizName: string;
|
bizName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SuggestionItemType = {
|
||||||
|
id: number;
|
||||||
|
domain: number;
|
||||||
|
name: string;
|
||||||
|
bizName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SuggestionType = {
|
||||||
|
dimensions: SuggestionItemType[];
|
||||||
|
metrics: SuggestionItemType[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
preview.pro.ant.design
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"/umi.css": "/webapp/umi.51119182.css",
|
|
||||||
"/umi.js": "/webapp/umi.5428eb1b.js",
|
|
||||||
"/public/CNAME": "/webapp/CNAME",
|
|
||||||
"/public/favicon.ico": "/webapp/favicon.ico",
|
|
||||||
"/public/home_bg.png": "/webapp/home_bg.png",
|
|
||||||
"/public/icons/icon-128x128.png": "/webapp/icons/icon-128x128.png",
|
|
||||||
"/public/icons/icon-192x192.png": "/webapp/icons/icon-192x192.png",
|
|
||||||
"/public/icons/icon-512x512.png": "/webapp/icons/icon-512x512.png",
|
|
||||||
"/index.html": "/webapp/index.html",
|
|
||||||
"/public/logo.svg": "/webapp/logo.svg",
|
|
||||||
"/public/pro_icon.svg": "/webapp/pro_icon.svg",
|
|
||||||
"/static/cloudEditor.svg": "/webapp/static/cloudEditor.1a9aa2c1.svg",
|
|
||||||
"/static/iconfont.woff2?t=1659425018463": "/webapp/static/iconfont.0ac2d58a.woff2",
|
|
||||||
"/static/iconfont.woff?t=1659425018463": "/webapp/static/iconfont.0de60a33.woff",
|
|
||||||
"/static/iconfont.ttf?t=1659425018463": "/webapp/static/iconfont.7ae6e4e0.ttf",
|
|
||||||
"/static/iconfont.svg?t=1659425018463": "/webapp/static/iconfont.92a3f736.svg",
|
|
||||||
"/public/supersonic.config.json": "/webapp/supersonic.config.json",
|
|
||||||
"/public/version.js": "/webapp/version.js"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 551 B |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,34 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta
|
|
||||||
http-equiv="Cache-Control"
|
|
||||||
content="no-cache, no-store, must-revalidate"
|
|
||||||
/>
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
|
||||||
/>
|
|
||||||
<title>超音数(SuperSonic)</title>
|
|
||||||
<link rel="icon" href="/webapp/favicon.ico" type="image/x-icon" />
|
|
||||||
<meta name="app_version" content="2023-07-30 23:33:26" />
|
|
||||||
<link rel="stylesheet" href="/webapp/umi.51119182.css" />
|
|
||||||
<script>
|
|
||||||
window.routerBase = "/webapp/";
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
//! umi version: 3.5.41
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<noscript>Out-of-the-box mid-stage front/design solution!</noscript>
|
|
||||||
|
|
||||||
<div id="root"></div>
|
|
||||||
<script src="/webapp/umi.5428eb1b.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 18" class="design-iconfont" width="128" height="128"><path d="M24.7272727,4.26325641e-14 L33.5127273,17.4545455 L26.5345455,17.4545455 L21.1236364,6.70181818 L24.7272727,4.26325641e-14 Z M17.52,4.26325641e-14 L21.1236364,6.70181818 L15.7127273,17.4545455 L8.73090909,17.4545455 L17.52,4.26325641e-14 Z M41.5890909,12.6945455 L43.9818182,17.4545455 L35.0909091,17.4545455 L32.6981818,12.6945455 L41.5890909,12.6945455 Z M12.68,6.32 L7.08,17.4545455 L0.498181818,17.4545455 L6.09818182,6.32 L12.68,6.32 Z M38.4145455,6.32 L40.9090909,11.2727273 L32.0181818,11.2727273 L29.5272727,6.32 L38.4145455,6.32 Z M15.7890909,0.141818182 L13.3963636,4.89818182 L-3.55271368e-14,4.89818182 L2.39272727,0.141818182 L15.7890909,0.141818182 Z M35.2690909,0.141818182 L37.6654545,4.89818182 L28.7745455,4.89818182 L26.3818182,0.141818182 L35.2690909,0.141818182 Z" fill-rule="evenodd" fill="#1890ff"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 953 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g>
|
|
||||||
<path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 677 B |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"env": ""
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
feVersion={
|
|
||||||
"commitId": "078a81038f60d1a220e26b78b67156ff406a484d",
|
|
||||||
"updateTime": "Sun Jul 30 2023 23:33:24 GMT+0800 (China Standard Time)"
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
rm -rf ./packages/supersonic-fe/src/.umi ./packages/supersonic-fe/src/.umi-production
|
||||||
|
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
npx lerna add supersonic-chat-sdk --scope supersonic-fe
|
npx lerna add supersonic-chat-sdk --scope supersonic-fe
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
rm -rf supersonic-webapp.tar.gz
|
rm -rf supersonic-webapp.tar.gz
|
||||||
|
|
||||||
|
rm -rf ./packages/supersonic-fe/src/.umi ./packages/supersonic-fe/src/.umi-production
|
||||||
|
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
npx lerna add supersonic-chat-sdk --scope supersonic-fe
|
npx lerna add supersonic-chat-sdk --scope supersonic-fe
|
||||||
|
|||||||