[feature](webapp) upgrade chat version

This commit is contained in:
williamhliu
2023-06-30 17:42:03 +08:00
parent 8639c23dc4
commit 805a59dddd
69 changed files with 1570 additions and 842 deletions

1
.gitignore vendored
View File

@@ -13,4 +13,5 @@ assembly/runtime/*
**/dist/ **/dist/
*.umi/ *.umi/
/assembly/deploy /assembly/deploy
/runtime
.flattened-pom.xml .flattened-pom.xml

View File

@@ -13,9 +13,6 @@
/dist /dist
package-lock.json
yarn.lock
# misc # misc
.DS_Store .DS_Store
.env.local .env.local

View File

@@ -1,12 +1,12 @@
{ {
"name": "supersonic-chat-sdk", "name": "supersonic-chat-sdk",
"version": "0.1.0", "version": "0.0.0",
"main": "dist/index.es.js", "main": "dist/index.es.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",
"unpkg": "dist/index.umd.js", "unpkg": "dist/index.umd.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"dependencies": { "dependencies": {
"antd": "^5.5.0", "antd": "^5.5.2",
"axios": "^1.4.0", "axios": "^1.4.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"echarts": "^5.4.2", "echarts": "^5.4.2",
@@ -19,7 +19,7 @@
"react-dom": ">=16.8.0" "react-dom": ">=16.8.0"
}, },
"scripts": { "scripts": {
"start": "npm run build-es", "start": "npm run start:dev",
"start:dev": "node scripts/start.js", "start:dev": "node scripts/start.js",
"clean": "rimraf ./dist", "clean": "rimraf ./dist",
"build": "npm run clean && npm run build-es", "build": "npm run clean && npm run build-es",

View File

@@ -1,4 +1,5 @@
import basicConfig from './rollup.config.mjs' import basicConfig from './rollup.config.mjs'
// import { terser } from "rollup-plugin-terser"
import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle" import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle"
const config = { const config = {
@@ -7,6 +8,9 @@ const config = {
{ {
file: 'dist/index.es.js', file: 'dist/index.es.js',
format: 'es', format: 'es',
// plugins: [
// terser()
// ],
}, },
], ],
plugins: [ plugins: [

View File

@@ -1,5 +1,5 @@
import basicConfig from './rollup.config.mjs' import basicConfig from './rollup.config.mjs'
import terser from '@rollup/plugin-terser'; import { terser } from '@rollup/plugin-terser'
import replace from '@rollup/plugin-replace' import replace from '@rollup/plugin-replace'
const config = { const config = {

View File

@@ -46,7 +46,7 @@ export type FilterItemType = {
name: string; name: string;
operator: string; operator: string;
type: string; type: string;
value: string; value: string[];
}; };
export type ChatContextType = { export type ChatContextType = {
@@ -57,7 +57,7 @@ export type ChatContextType = {
dimensions: FieldType[]; dimensions: FieldType[];
metrics: FieldType[]; metrics: FieldType[];
entity: number; entity: number;
filters: FilterItemType[]; dimensionFilters: FilterItemType[];
}; };
export enum MsgValidTypeEnum { export enum MsgValidTypeEnum {
@@ -67,11 +67,22 @@ export enum MsgValidTypeEnum {
INVALID = 3, INVALID = 3,
}; };
export type InstructionResonseType = {
description: string;
instructionConfig: {
showElements: { elementId: string, params: any }[];
showType: string;
relaShowElements: { elementId: string, params: any }[];
relaShowType: string;
};
instructionId: number;
instructionType: string;
name: string;
}
export type MsgDataType = { export type MsgDataType = {
id: number; id: number;
question: string; question: string;
aggregateType: string;
appletResponse: string;
chatContext: ChatContextType; chatContext: ChatContextType;
entityInfo: EntityInfoType; entityInfo: EntityInfoType;
queryAuthorization: any; queryAuthorization: any;
@@ -80,6 +91,7 @@ export type MsgDataType = {
queryId: number; queryId: number;
queryMode: string; queryMode: string;
queryState: MsgValidTypeEnum; queryState: MsgValidTypeEnum;
response: InstructionResonseType;
}; };
export type QueryDataType = { export type QueryDataType = {

View File

@@ -8,7 +8,7 @@ type Props = {
const Text: React.FC<Props> = ({ data }) => { const Text: React.FC<Props> = ({ data }) => {
const prefixCls = `${PREFIX_CLS}-item`; const prefixCls = `${PREFIX_CLS}-item`;
return ( return (
<Message position="left" bubbleClassName={`${prefixCls}-text-bubble`} noWaterMark> <Message position="left" bubbleClassName={`${prefixCls}-text-bubble`}>
<div className={`${prefixCls}-text`}>{data}</div> <div className={`${prefixCls}-text`}>{data}</div>
</Message> </Message>
); );

View File

@@ -1,49 +1,48 @@
import { MsgDataType, MsgValidTypeEnum, SuggestionDataType } from '../../common/type'; import { MsgDataType, MsgValidTypeEnum } 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 { querySuggestionInfo, chatQuery } from '../../service'; import { chatQuery } from '../../service';
import { MSG_VALID_TIP, PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants'; import { MSG_VALID_TIP, PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
import Text from './Text'; import Text from './Text';
import Suggestion from '../Suggestion';
import Tools from '../Tools'; import Tools from '../Tools';
import SemanticDetail from '../SemanticDetail'; import SemanticDetail from '../SemanticDetail';
import IconFont from '../IconFont';
type Props = { type Props = {
msg: string; msg: string;
followQuestions?: string[];
conversationId?: number; conversationId?: number;
classId?: number; domainId?: number;
isLastMessage?: boolean; isLastMessage?: boolean;
suggestionEnable?: boolean;
msgData?: MsgDataType; msgData?: MsgDataType;
onLastMsgDataLoaded?: (data: MsgDataType) => void; isMobileMode?: boolean;
triggerResize?: boolean;
onMsgDataLoaded?: (data: MsgDataType) => void;
onSelectSuggestion?: (value: string) => void; onSelectSuggestion?: (value: string) => void;
onUpdateMessageScroll?: () => void; onUpdateMessageScroll?: () => void;
}; };
const ChatItem: React.FC<Props> = ({ const ChatItem: React.FC<Props> = ({
msg, msg,
followQuestions,
conversationId, conversationId,
classId, domainId,
isLastMessage, isLastMessage,
suggestionEnable, isMobileMode,
triggerResize,
msgData, msgData,
onLastMsgDataLoaded, onMsgDataLoaded,
onSelectSuggestion, onSelectSuggestion,
onUpdateMessageScroll, onUpdateMessageScroll,
}) => { }) => {
const [data, setData] = useState<MsgDataType>(); const [data, setData] = useState<MsgDataType>();
const [suggestionData, setSuggestionData] = useState<SuggestionDataType>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [metricInfoList, setMetricInfoList] = useState<any[]>([]); const [metricInfoList, setMetricInfoList] = useState<any[]>([]);
const [tip, setTip] = useState(''); const [tip, setTip] = useState('');
const setMsgData = (value: MsgDataType) => {
setData(value);
};
const updateData = (res: Result<MsgDataType>) => { const updateData = (res: Result<MsgDataType>) => {
if (res.code === 401) { if (res.code === 401 || res.code === 412) {
setTip(res.msg); setTip(res.msg);
return false; return false;
} }
@@ -51,13 +50,13 @@ const ChatItem: React.FC<Props> = ({
setTip(PARSE_ERROR_TIP); setTip(PARSE_ERROR_TIP);
return false; return false;
} }
const { queryColumns, queryResults, queryState } = res.data || {}; const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
if (queryState !== MsgValidTypeEnum.NORMAL && queryState !== MsgValidTypeEnum.EMPTY) { if (queryState !== MsgValidTypeEnum.NORMAL && queryState !== MsgValidTypeEnum.EMPTY) {
setTip(MSG_VALID_TIP[queryState || MsgValidTypeEnum.INVALID]); setTip(MSG_VALID_TIP[queryState || MsgValidTypeEnum.INVALID]);
return false; return false;
} }
if (queryColumns && queryColumns.length > 0 && queryResults) { if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
setMsgData(res.data); setData(res.data);
setTip(''); setTip('');
return true; return true;
} }
@@ -65,41 +64,20 @@ const ChatItem: React.FC<Props> = ({
return false; return false;
}; };
const updateSuggestionData = (semanticRes: MsgDataType, suggestionRes: any) => {
const { aggregateType, queryColumns, entityInfo } = semanticRes;
setSuggestionData({
currentAggregateType: aggregateType,
columns: queryColumns || [],
mainEntity: entityInfo,
suggestions: suggestionRes,
});
};
const getSuggestions = async (domainId: number, semanticResData: MsgDataType) => {
if (!domainId) {
return;
}
const res = await querySuggestionInfo(domainId);
updateSuggestionData(semanticResData, res.data.data);
};
const onSendMsg = async () => { const onSendMsg = async () => {
setLoading(true); setLoading(true);
const semanticRes = await chatQuery(msg, conversationId, classId); const semanticRes = await chatQuery(msg, conversationId, domainId);
updateData(semanticRes.data); updateData(semanticRes.data);
// if (suggestionEnable && semanticValid) { if (onMsgDataLoaded) {
// const semanticResData = semanticRes.data.data; onMsgDataLoaded(semanticRes.data.data);
// await getSuggestions(semanticResData.entityInfo?.domainInfo?.itemId, semanticRes.data.data);
// } else {
// setSuggestionData(undefined);
// }
if (onLastMsgDataLoaded) {
onLastMsgDataLoaded(semanticRes.data.data);
} }
setLoading(false); setLoading(false);
}; };
useEffect(() => { useEffect(() => {
if (data !== undefined) {
return;
}
if (msgData) { if (msgData) {
updateData({ code: 200, data: msgData, msg: 'success' }); updateData({ code: 200, data: msgData, msg: 'success' });
} else if (msg) { } else if (msg) {
@@ -107,15 +85,27 @@ const ChatItem: React.FC<Props> = ({
} }
}, [msg, msgData]); }, [msg, msgData]);
const prefixCls = `${PREFIX_CLS}-item`;
if (loading) { if (loading) {
return <Typing />; return (
<div className={prefixCls}>
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
<Typing />
</div>
);
} }
if (tip) { if (tip) {
return <Text data={tip} />; return (
<div className={prefixCls}>
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
<Text data={tip} />
</div>
);
} }
if (!data) { if (!data || data.queryMode === 'INSTRUCTION') {
return null; return null;
} }
@@ -126,26 +116,33 @@ const ChatItem: React.FC<Props> = ({
} }
}; };
const prefixCls = `${PREFIX_CLS}-item`;
return ( return (
<div> <div className={prefixCls}>
<ChatMsg data={data} onCheckMetricInfo={onCheckMetricInfo} /> <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
<Tools isLastMessage={isLastMessage} /> <div className={`${prefixCls}-content`}>
{suggestionEnable && suggestionData && isLastMessage && ( <ChatMsg
<Suggestion {...suggestionData} onSelect={onSelectSuggestion} /> question={msg}
)} followQuestions={followQuestions}
<div className={`${prefixCls}-metric-info-list`}> data={data}
{metricInfoList.map(item => ( isMobileMode={isMobileMode}
<SemanticDetail triggerResize={triggerResize}
dataSource={item} onCheckMetricInfo={onCheckMetricInfo}
onDimensionSelect={(value: string) => { />
if (onSelectSuggestion) { <Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
onSelectSuggestion(value); {metricInfoList.length > 0 && (
} <div className={`${prefixCls}-metric-info-list`}>
}} {metricInfoList.map(item => (
/> <SemanticDetail
))} dataSource={item}
onDimensionSelect={(value: string) => {
if (onSelectSuggestion) {
onSelectSuggestion(value);
}
}}
/>
))}
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -3,6 +3,26 @@
@chat-item-prefix-cls: ~'@{supersonic-chat-prefix}-item'; @chat-item-prefix-cls: ~'@{supersonic-chat-prefix}-item';
.@{chat-item-prefix-cls} { .@{chat-item-prefix-cls} {
display: flex;
&-avatar {
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
width: 40px;
height: 40px;
margin-right: 6px;
border-radius: 50%;
color: var(--chat-blue);
background-color: #fff;
}
&-content {
// flex: 1;
width: calc(100% - 50px);
}
&-metric-info-list { &-metric-info-list {
margin-top: 30px; margin-top: 30px;
display: flex; display: flex;

View File

@@ -8,10 +8,11 @@ import NoPermissionChart from '../NoPermissionChart';
type Props = { type Props = {
data: MsgDataType; data: MsgDataType;
triggerResize?: boolean;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (domain: string) => void;
}; };
const BarChart: React.FC<Props> = ({ data, onApplyAuth }) => { const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
const chartRef = useRef<any>(); const chartRef = useRef<any>();
const [instance, setInstance] = useState<ECharts>(); const [instance, setInstance] = useState<ECharts>();
@@ -133,7 +134,13 @@ const BarChart: React.FC<Props> = ({ data, onApplyAuth }) => {
} }
}, [queryResults]); }, [queryResults]);
if (!metricColumn?.authorized) { useEffect(() => {
if (triggerResize && instance) {
instance.resize();
}
}, [triggerResize]);
if (metricColumn && !metricColumn?.authorized) {
return ( return (
<NoPermissionChart <NoPermissionChart
domain={entityInfo?.domainInfo.name || ''} domain={entityInfo?.domainInfo.name || ''}

View File

@@ -1,67 +1,55 @@
import { EntityInfoType, ChatContextType } from '../../../common/type'; import { EntityInfoType, ChatContextType } from '../../../common/type';
import moment from 'moment';
import { PREFIX_CLS } from '../../../common/constants'; import { PREFIX_CLS } from '../../../common/constants';
type Props = { type Props = {
position: 'left' | 'right'; position: 'left' | 'right';
width?: number | string; width?: number | string;
height?: number | string; height?: number | string;
title?: string;
followQuestions?: string[];
bubbleClassName?: string; bubbleClassName?: string;
noWaterMark?: boolean;
chatContext?: ChatContextType; chatContext?: ChatContextType;
entityInfo?: EntityInfoType; entityInfo?: EntityInfoType;
tip?: string;
aggregator?: string;
noTime?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
isMobileMode?: boolean;
}; };
const Message: React.FC<Props> = ({ const Message: React.FC<Props> = ({
position, position,
width, width,
height, height,
title,
followQuestions,
children, children,
bubbleClassName, bubbleClassName,
chatContext, chatContext,
entityInfo, entityInfo,
aggregator, isMobileMode,
noTime,
}) => { }) => {
const { aggType, dateInfo, filters, metrics, domainName } = chatContext || {}; const { dimensionFilters, domainName } = chatContext || {};
const prefixCls = `${PREFIX_CLS}-message`; const prefixCls = `${PREFIX_CLS}-message`;
const timeSection = const entityInfoList =
!noTime && dateInfo?.text ? ( entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || [];
dateInfo.text
) : (
<div>{`${moment(dateInfo?.endDate).diff(dateInfo?.startDate, 'days') + 1}`}</div>
);
const metricSection = const hasFilterSection =
metrics && dimensionFilters && dimensionFilters.length > 0 && entityInfoList.length === 0;
metrics.map((metric, index) => {
let metricNode = <span className={`${PREFIX_CLS}-metric`}>{metric.name}</span>;
return (
<>
{metricNode}
{index < metrics.length - 1 && <span></span>}
</>
);
});
const aggregatorSection = aggregator !== 'tag' && aggType !== 'NONE' && aggType;
const hasFilterSection = filters && filters.length > 0;
const filterSection = hasFilterSection && ( const filterSection = hasFilterSection && (
<div className={`${prefixCls}-filter-section`}> <div className={`${prefixCls}-filter-section`}>
<div className={`${prefixCls}-field-name`}></div> <div className={`${prefixCls}-field-name`}></div>
<div className={`${prefixCls}-filter-values`}> <div className={`${prefixCls}-filter-values`}>
{filters.map(filterItem => { {dimensionFilters.map(filterItem => {
const filterValue =
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
return ( return (
<div className={`${prefixCls}-filter-item`} key={filterItem.name}> <div
{filterItem.name}{filterItem.value} className={`${prefixCls}-filter-item`}
key={filterItem.name}
title={filterValue.join('、')}
>
{filterItem.name}{filterValue.join('、')}
</div> </div>
); );
})} })}
@@ -69,14 +57,15 @@ const Message: React.FC<Props> = ({
</div> </div>
); );
const entityInfoList = const leftTitle = title
entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || []; ? followQuestions && followQuestions.length > 0
? `多轮对话:${[title, ...followQuestions].join(' ← ')}`
const hasEntityInfoSection = : `单轮对话:${title}`
entityInfoList.length > 0 && chatContext && chatContext.dimensions?.length > 0; : '';
return ( return (
<div className={prefixCls}> <div className={prefixCls}>
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
<div className={`${prefixCls}-content`}> <div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-body`}> <div className={`${prefixCls}-body`}>
<div <div
@@ -86,31 +75,30 @@ const Message: React.FC<Props> = ({
e.stopPropagation(); e.stopPropagation();
}} }}
> >
{position === 'left' && chatContext && ( {position === 'left' && title && (
<div className={`${prefixCls}-top-bar`}> <div className={`${prefixCls}-top-bar`} title={leftTitle}>
{domainName} {leftTitle}
{/* {dimensionSection} */}
{timeSection}
{metricSection}
{aggregatorSection}
{/* {tipSection} */}
</div> </div>
)} )}
{(hasEntityInfoSection || hasFilterSection) && ( {(entityInfoList.length > 0 || hasFilterSection) && (
<div className={`${prefixCls}-info-bar`}> <div className={`${prefixCls}-info-bar`}>
{hasEntityInfoSection && ( {filterSection}
{entityInfoList.length > 0 && (
<div className={`${prefixCls}-main-entity-info`}> <div className={`${prefixCls}-main-entity-info`}>
{entityInfoList.slice(0, 3).map(dimension => { {entityInfoList.slice(0, 4).map(dimension => {
return ( return (
<div className={`${prefixCls}-info-item`} key={dimension.bizName}> <div className={`${prefixCls}-info-item`} key={dimension.bizName}>
<div className={`${prefixCls}-info-name`}>{dimension.name}</div> <div className={`${prefixCls}-info-name`}>{dimension.name}</div>
<div className={`${prefixCls}-info-value`}>{dimension.value}</div> {dimension.bizName.includes('photo') ? (
<img width={40} height={40} src={dimension.value} alt="" />
) : (
<div className={`${prefixCls}-info-value`}>{dimension.value}</div>
)}
</div> </div>
); );
})} })}
</div> </div>
)} )}
{filterSection}
</div> </div>
)} )}
<div className={`${prefixCls}-children`}>{children}</div> <div className={`${prefixCls}-children`}>{children}</div>

View File

@@ -3,6 +3,13 @@
@msg-prefix-cls: ~'@{supersonic-chat-prefix}-message'; @msg-prefix-cls: ~'@{supersonic-chat-prefix}-message';
.@{msg-prefix-cls} { .@{msg-prefix-cls} {
&-domain-name {
color: var(--text-color);
margin-bottom: 2px;
margin-left: 4px;
font-weight: 500;
}
&-content { &-content {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -24,15 +31,14 @@
} }
&-top-bar { &-top-bar {
display: flex; position: relative;
align-items: center;
max-width: 100%; max-width: 100%;
padding: 4px 0 8px; padding: 4px 0 8px;
overflow-x: auto; color: var(--text-color-third);
color: var(--text-color); font-size: 13px;
font-weight: 500;
font-size: 14px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
border-bottom: 1px solid rgba(0, 0, 0, 0.03); border-bottom: 1px solid rgba(0, 0, 0, 0.03);
} }
@@ -44,11 +50,21 @@
font-size: 13px; font-size: 13px;
} }
&-filter-values {
display: flex;
align-items: center;
column-gap: 6px;
}
&-filter-item { &-filter-item {
padding: 2px 12px; padding: 2px 12px;
color: var(--text-color-secondary); color: var(--text-color-secondary);
background-color: #edf2f2; background-color: #edf2f2;
border-radius: 13px; border-radius: 13px;
max-width: 200px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
&-tip { &-tip {
@@ -58,10 +74,16 @@
&-info-bar { &-info-bar {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
row-gap: 12px;
flex-wrap: wrap;
margin-top: 20px; margin-top: 20px;
column-gap: 20px; column-gap: 20px;
color: var(--text-color-secondary);
background: rgba(133, 156, 241, 0.1);
padding: 4px 12px;
width: fit-content;
border-radius: 8px;
} }
&-main-entity-info { &-main-entity-info {
@@ -70,6 +92,7 @@
align-items: center; align-items: center;
font-size: 13px; font-size: 13px;
column-gap: 20px; column-gap: 20px;
row-gap: 10px;
} }
&-info-item { &-info-item {
@@ -77,12 +100,12 @@
align-items: center; align-items: center;
} }
&-info-Name { &-info-name {
color: var(--text-color-fourth); color: var(--text-color-third);
} }
&-info-value { &-info-value {
color: var(--text-color-secondary); color: var(--text-color);
} }
} }

View File

@@ -22,7 +22,7 @@ const MetricCard: React.FC<Props> = ({ data, onApplyAuth }) => {
{/* <div className={`${prefixCls}-date-range`}> {/* <div className={`${prefixCls}-date-range`}>
{startTime === endTime ? startTime : `${startTime} ~ ${endTime}`} {startTime === endTime ? startTime : `${startTime} ~ ${endTime}`}
</div> */} </div> */}
{!indicatorColumn?.authorized ? ( {indicatorColumn && !indicatorColumn?.authorized ? (
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} /> <ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
) : ( ) : (
<div className={`${prefixCls}-indicator-value`}> <div className={`${prefixCls}-indicator-value`}>

View File

@@ -19,6 +19,7 @@ type Props = {
categoryColumnName: string; categoryColumnName: string;
metricField: ColumnType; metricField: ColumnType;
resultList: any[]; resultList: any[];
triggerResize?: boolean;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (domain: string) => void;
}; };
@@ -28,6 +29,7 @@ const MetricTrendChart: React.FC<Props> = ({
categoryColumnName, categoryColumnName,
metricField, metricField,
resultList, resultList,
triggerResize,
onApplyAuth, onApplyAuth,
}) => { }) => {
const chartRef = useRef<any>(); const chartRef = useRef<any>();
@@ -40,6 +42,7 @@ const MetricTrendChart: React.FC<Props> = ({
setInstance(instanceObj); setInstance(instanceObj);
} else { } else {
instanceObj = instance; instanceObj = instance;
instanceObj.clear();
} }
const valueColumnName = metricField.nameEn; const valueColumnName = metricField.nameEn;
@@ -51,13 +54,13 @@ const MetricTrendChart: React.FC<Props> = ({
endDate && endDate &&
(dateColumnName.includes('date') || dateColumnName.includes('month')) (dateColumnName.includes('date') || dateColumnName.includes('month'))
? normalizeTrendData( ? normalizeTrendData(
groupDataValue[key], groupDataValue[key],
dateColumnName, dateColumnName,
valueColumnName, valueColumnName,
startDate, startDate,
endDate, endDate,
dateColumnName.includes('month') ? 'months' : 'days' dateColumnName.includes('month') ? 'months' : 'days'
) )
: groupDataValue[key].reverse(); : groupDataValue[key].reverse();
return result; return result;
}, {}); }, {});
@@ -114,8 +117,8 @@ const MetricTrendChart: React.FC<Props> = ({
return value === 0 return value === 0
? 0 ? 0
: metricField.dataFormatType === 'percent' : metricField.dataFormatType === 'percent'
? `${formatByDecimalPlaces(value, metricField.dataFormat?.decimalPlaces || 2)}%` ? `${formatByDecimalPlaces(value, metricField.dataFormat?.decimalPlaces || 2)}%`
: getFormattedValue(value); : getFormattedValue(value);
}, },
}, },
}, },
@@ -135,11 +138,11 @@ const MetricTrendChart: React.FC<Props> = ({
item.value === '' item.value === ''
? '-' ? '-'
: metricField.dataFormatType === 'percent' : metricField.dataFormatType === 'percent'
? `${formatByDecimalPlaces( ? `${formatByDecimalPlaces(
item.value, item.value,
metricField.dataFormat?.decimalPlaces || 2 metricField.dataFormat?.decimalPlaces || 2
)}%` )}%`
: getFormattedValue(item.value) : getFormattedValue(item.value)
}</span></div>` }</span></div>`
) )
.join(''); .join('');
@@ -181,6 +184,12 @@ const MetricTrendChart: React.FC<Props> = ({
} }
}, [resultList, metricField]); }, [resultList, metricField]);
useEffect(() => {
if (triggerResize && instance) {
instance.resize();
}
}, [triggerResize]);
const prefixCls = `${CLS_PREFIX}-metric-trend`; const prefixCls = `${CLS_PREFIX}-metric-trend`;
return ( return (

View File

@@ -1,30 +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, MsgDataType } from '../../../common/type'; import { ColumnType, FieldType, MsgDataType } from '../../../common/type';
import { groupByColumn, 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 SemanticInfoPopover from '../SemanticInfoPopover';
type Props = { type Props = {
data: MsgDataType; data: MsgDataType;
triggerResize?: boolean;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (domain: string) => void;
onCheckMetricInfo?: (data: any) => void; onCheckMetricInfo?: (data: any) => void;
}; };
const MetricTrend: React.FC<Props> = ({ data, onApplyAuth, onCheckMetricInfo }) => { const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onCheckMetricInfo }) => {
const { queryColumns, queryResults, entityInfo, chatContext } = data; const { queryColumns, queryResults, entityInfo, chatContext } = data;
const [columns, setColumns] = useState<ColumnType[]>(queryColumns); const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
const metricFields = columns.filter((column: any) => column.showType === 'NUMBER') || []; const currentMetricField = columns.find((column: any) => column.showType === 'NUMBER');
const [currentMetricField, setCurrentMetricField] = useState<ColumnType>(metricFields[0]); const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
const [onlyOneDate, setOnlyOneDate] = useState(false);
const [trendData, setTrendData] = useState(data);
const [dataSource, setDataSource] = useState<any[]>(queryResults); const [dataSource, setDataSource] = useState<any[]>(queryResults);
const [mergeMetric, setMergeMetric] = useState(false);
const [currentDateOption, setCurrentDateOption] = useState<number>(); const [currentDateOption, setCurrentDateOption] = useState<number>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -35,66 +32,17 @@ const MetricTrend: React.FC<Props> = ({ data, onApplyAuth, onCheckMetricInfo })
const categoryColumnName = const categoryColumnName =
columns.find((column: any) => column.showType === 'CATEGORY')?.nameEn || ''; columns.find((column: any) => column.showType === 'CATEGORY')?.nameEn || '';
const getColumns = () => {
const categoryFieldData = groupByColumn(dataSource, categoryColumnName);
const result = [dateField];
const columnsValue = Object.keys(categoryFieldData).map(item => ({
authorized: currentMetricField.authorized,
name: item !== 'undefined' ? item : currentMetricField.name,
nameEn: `${item}${currentMetricField.name}`,
showType: 'NUMBER',
type: 'NUMBER',
}));
return result.concat(columnsValue);
};
const getResultList = () => {
return [
{
[dateField.nameEn]: dataSource[0][dateField.nameEn],
...dataSource.reduce((result, item) => {
result[`${item[categoryColumnName]}${currentMetricField.name}`] =
item[currentMetricField.nameEn];
return result;
}, {}),
},
];
};
useEffect(() => { useEffect(() => {
setDataSource(queryResults); setDataSource(queryResults);
}, [queryResults]); }, [queryResults]);
useEffect(() => { const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES[0];
let onlyOneDateValue = false;
let dataValue = trendData;
if (dateColumnName && dataSource.length > 0) {
const dateFieldData = groupByColumn(dataSource, dateColumnName);
onlyOneDateValue =
Object.keys(dateFieldData).length === 1 && Object.keys(dateFieldData)[0] !== undefined;
if (onlyOneDateValue) {
if (categoryColumnName !== '') {
dataValue = {
...trendData,
queryColumns: getColumns(),
queryResults: getResultList(),
};
} else {
setMergeMetric(true);
}
}
}
setOnlyOneDate(onlyOneDateValue);
setTrendData(dataValue);
}, [currentMetricField]);
const dateOptions = DATE_TYPES[chatContext.dateInfo?.period] || DATE_TYPES[0]; const onLoadData = async (value: any) => {
const onLoadData = async (value: number) => {
setLoading(true); setLoading(true);
const { data } = await queryData({ const { data } = await queryData({
...chatContext, ...chatContext,
dateInfo: { ...chatContext.dateInfo, unit: value }, ...value,
}); });
setLoading(false); setLoading(false);
if (data.code === 200) { if (data.code === 200) {
@@ -105,20 +53,21 @@ const MetricTrend: React.FC<Props> = ({ data, onApplyAuth, onCheckMetricInfo })
const selectDateOption = (dateOption: number) => { const selectDateOption = (dateOption: number) => {
setCurrentDateOption(dateOption); setCurrentDateOption(dateOption);
// const { domainName, dimensions, metrics, aggType, filters } = chatContext || {}; onLoadData({
// const dimensionSection = dimensions?.join('、') || ''; metrics: [activeMetricField],
// const metricSection = metrics?.join('、') || ''; dateInfo: { ...chatContext?.dateInfo, unit: dateOption },
// const aggregatorSection = aggType || ''; });
// const filterSection = filters
// .reduce((result, dimensionName) => {
// result = result.concat(dimensionName);
// return result;
// }, [])
// .join('、');
onLoadData(dateOption);
}; };
if (metricFields.length === 0) { const onSwitchMetric = (metricField: FieldType) => {
setActiveMetricField(metricField);
onLoadData({
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
metrics: [metricField],
});
};
if (!currentMetricField) {
return null; return null;
} }
@@ -127,64 +76,66 @@ const MetricTrend: React.FC<Props> = ({ data, onApplyAuth, onCheckMetricInfo })
return ( return (
<div className={prefixCls}> <div className={prefixCls}>
<div className={`${prefixCls}-charts`}> <div className={`${prefixCls}-charts`}>
{!onlyOneDate && ( <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`, { [`${prefixCls}-date-active`]: dateOption.value === currentDateOption,
[`${prefixCls}-date-active`]: dateOption.value === currentDateOption, [`${prefixCls}-date-mobile`]: isMobile,
[`${prefixCls}-date-mobile`]: isMobile, });
}); return (
return ( <>
<> <div
<div key={dateOption.value}
key={dateOption.value} className={dateOptionClass}
className={dateOptionClass} onClick={() => {
onClick={() => { selectDateOption(dateOption.value);
selectDateOption(dateOption.value); }}
}} >
> {dateOption.label}
{dateOption.label} {dateOption.value === currentDateOption && (
{dateOption.value === currentDateOption && ( <div className={`${prefixCls}-active-identifier`} />
<div className={`${prefixCls}-active-identifier`} />
)}
</div>
{index !== dateOptions.length - 1 && (
<div className={`${prefixCls}-date-option-divider`} />
)} )}
</> </div>
); {index !== dateOptions.length - 1 && (
})} <div className={`${prefixCls}-date-option-divider`} />
</div> )}
)} </>
{metricFields.length > 1 && !mergeMetric && ( );
})}
</div>
{chatContext.metrics.length > 0 && (
<div className={`${prefixCls}-metric-fields`}> <div className={`${prefixCls}-metric-fields`}>
{metricFields.map((metricField: ColumnType) => { {chatContext.metrics.map((metricField: FieldType) => {
const metricFieldClass = classNames(`${prefixCls}-metric-field`, { const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
[`${prefixCls}-metric-field-active`]: [`${prefixCls}-metric-field-active`]:
currentMetricField?.nameEn === metricField.nameEn, activeMetricField?.bizName === metricField.bizName &&
chatContext.metrics.length > 1,
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
}); });
return ( return (
<div <div
className={metricFieldClass} className={metricFieldClass}
key={metricField.nameEn} key={metricField.bizName}
onClick={() => { onClick={() => {
setCurrentMetricField(metricField); if (chatContext.metrics.length > 1) {
onSwitchMetric(metricField);
}
}} }}
> >
<SemanticInfoPopover {/* <SemanticInfoPopover
classId={chatContext.domainId} classId={chatContext.domainId}
uniqueId={metricField.nameEn} uniqueId={metricField.bizName}
onDetailBtnClick={onCheckMetricInfo} onDetailBtnClick={onCheckMetricInfo}
> > */}
{metricField.name} {metricField.name}
</SemanticInfoPopover> {/* </SemanticInfoPopover> */}
</div> </div>
); );
})} })}
</div> </div>
)} )}
{onlyOneDate ? ( {dataSource?.length === 1 ? (
<Table data={trendData} onApplyAuth={onApplyAuth} /> <Table data={data} onApplyAuth={onApplyAuth} />
) : ( ) : (
<Spin spinning={loading}> <Spin spinning={loading}>
<MetricTrendChart <MetricTrendChart
@@ -193,6 +144,7 @@ const MetricTrend: React.FC<Props> = ({ data, onApplyAuth, onCheckMetricInfo })
categoryColumnName={categoryColumnName} categoryColumnName={categoryColumnName}
metricField={currentMetricField} metricField={currentMetricField}
resultList={dataSource} resultList={dataSource}
triggerResize={triggerResize}
onApplyAuth={onApplyAuth} onApplyAuth={onApplyAuth}
/> />
</Spin> </Spin>

View File

@@ -81,6 +81,17 @@
background-color: var(--chat-blue); background-color: var(--chat-blue);
} }
&-metric-field-single {
padding-left: 0;
font-weight: 500;
cursor: default;
color: var(--text-color-secondary);
&:hover {
color: var(--text-color-secondary);
}
}
&-date-options { &-date-options {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -56,14 +56,21 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
} }
); );
const getRowClassName = (_: any, index: number) => {
return index % 2 !== 0 ? `${prefixCls}-even-row` : '';
};
return ( return (
<div className={prefixCls}> <div className={prefixCls}>
<AntTable <AntTable
pagination={queryResults.length <= 10 ? false : undefined} pagination={
size={queryResults.length === 1 ? 'middle' : 'small'} queryResults.length <= 10 ? false : { defaultPageSize: 10, position: ['bottomCenter'] }
}
columns={tableColumns} columns={tableColumns}
dataSource={queryResults} dataSource={queryResults}
style={{ width: '100%' }} style={{ width: '100%' }}
scroll={{ x: 'max-content' }}
rowClassName={getRowClassName}
/> />
</div> </div>
); );

View File

@@ -16,6 +16,10 @@
width: 100%; width: 100%;
} }
&-even-row {
background-color: #fbfbfb;
}
.ant-table-container table > thead > tr:first-child th:first-child { .ant-table-container table > thead > tr:first-child th:first-child {
border-top-left-radius: 12px !important; border-top-left-radius: 12px !important;
border-bottom-left-radius: 12px !important; border-bottom-left-radius: 12px !important;

View File

@@ -7,12 +7,23 @@ import Table from './Table';
import { MsgDataType } from '../../common/type'; import { MsgDataType } from '../../common/type';
type Props = { type Props = {
question: string;
followQuestions?: string[];
data: MsgDataType; data: MsgDataType;
isMobileMode?: boolean;
triggerResize?: boolean;
onCheckMetricInfo?: (data: any) => void; onCheckMetricInfo?: (data: any) => void;
}; };
const ChatMsg: React.FC<Props> = ({ data, onCheckMetricInfo }) => { const ChatMsg: React.FC<Props> = ({
const { aggregateType, queryColumns, queryResults, chatContext, entityInfo } = data; question,
followQuestions,
data,
isMobileMode,
triggerResize,
onCheckMetricInfo,
}) => {
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
if (!queryColumns || !queryResults) { if (!queryColumns || !queryResults) {
return null; return null;
@@ -24,20 +35,30 @@ const ChatMsg: React.FC<Props> = ({ data, onCheckMetricInfo }) => {
const metricFields = queryColumns.filter(item => item.showType === 'NUMBER'); const metricFields = queryColumns.filter(item => item.showType === 'NUMBER');
const getMsgContent = () => { const getMsgContent = () => {
if (categoryField.length > 1 || aggregateType === 'tag') { if (
categoryField.length > 1 ||
queryMode === 'ENTITY_DETAIL' ||
queryMode === 'ENTITY_DIMENSION'
) {
return <Table data={data} />; return <Table data={data} />;
} }
if (dateField && metricFields.length > 0) { if (dateField && metricFields.length > 0) {
return <MetricTrend data={data} onCheckMetricInfo={onCheckMetricInfo} />; return (
<MetricTrend
data={data}
triggerResize={triggerResize}
onCheckMetricInfo={onCheckMetricInfo}
/>
);
} }
if (singleData) { if (singleData) {
return <MetricCard data={data} />; return <MetricCard data={data} />;
} }
return <Bar data={data} />; return <Bar data={data} triggerResize={triggerResize} />;
}; };
let width = '100%'; let width = '100%';
if ((categoryField.length > 1 || aggregateType === 'tag') && !isMobile) { if (categoryField.length > 1 && !isMobile && !isMobileMode) {
if (queryColumns.length === 1) { if (queryColumns.length === 1) {
width = '600px'; width = '600px';
} else if (queryColumns.length === 2) { } else if (queryColumns.length === 2) {
@@ -50,8 +71,9 @@ const ChatMsg: React.FC<Props> = ({ data, onCheckMetricInfo }) => {
position="left" position="left"
chatContext={chatContext} chatContext={chatContext}
entityInfo={entityInfo} entityInfo={entityInfo}
aggregator={aggregateType} title={question}
tip={''} followQuestions={followQuestions}
isMobileMode={isMobileMode}
width={width} width={width}
> >
{getMsgContent()} {getMsgContent()}

View File

@@ -0,0 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4120566_hsbqfckf8tl.js',
});
export default IconFont;

View File

@@ -17,7 +17,7 @@ const SemanticDetail: React.FC<Props> = ({ dataSource, onDimensionSelect }) => {
const semanticDetailCls = `${CLS_PREFIX}-semantic-detail`; const semanticDetailCls = `${CLS_PREFIX}-semantic-detail`;
return ( return (
<Message position="left" width="100%" noTime> <Message position="left" width="100%">
<div> <div>
<div> <div>
<Row> <Row>

View File

@@ -79,7 +79,7 @@ const Suggestion: React.FC<Props> = ({
return ( return (
<div className={suggestionClass}> <div className={suggestionClass}>
<Message position="left" width="fit-content" noWaterMark> <Message position="left" width="fit-content">
<div className={`${prefixCls}-tip`}></div> <div className={`${prefixCls}-tip`}></div>
{metricList.length > 0 && ( {metricList.length > 0 && (
<div className={`${prefixCls}-content-section`}> <div className={`${prefixCls}-content-section`}>

View File

@@ -2,12 +2,15 @@ import { isMobile } from '../../utils/utils';
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons'; import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
import { Button, message } from 'antd'; import { Button, message } from 'antd';
import { CLS_PREFIX } from '../../common/constants'; import { CLS_PREFIX } from '../../common/constants';
import { MsgDataType } from '../../common/type';
type Props = { type Props = {
data: MsgDataType;
isLastMessage?: boolean; isLastMessage?: boolean;
isMobileMode?: boolean;
}; };
const Tools: React.FC<Props> = ({ isLastMessage }) => { const Tools: React.FC<Props> = ({ data, isLastMessage, isMobileMode }) => {
const prefixCls = `${CLS_PREFIX}-tools`; const prefixCls = `${CLS_PREFIX}-tools`;
const changeChart = () => { const changeChart = () => {
@@ -18,10 +21,6 @@ const Tools: React.FC<Props> = ({ isLastMessage }) => {
message.info('正在开发中,敬请期待'); message.info('正在开发中,敬请期待');
}; };
const lockDomain = () => {
message.info('正在开发中,敬请期待');
};
const like = () => { const like = () => {
message.info('正在开发中,敬请期待'); message.info('正在开发中,敬请期待');
}; };
@@ -30,12 +29,6 @@ const Tools: React.FC<Props> = ({ isLastMessage }) => {
message.info('正在开发中,敬请期待'); message.info('正在开发中,敬请期待');
}; };
const lockDomainSection = isLastMessage && (
<Button shape="round" onClick={lockDomain}>
</Button>
);
const feedbackSection = isLastMessage && ( const feedbackSection = isLastMessage && (
<div className={`${prefixCls}-feedback`}> <div className={`${prefixCls}-feedback`}>
<div></div> <div></div>
@@ -44,25 +37,19 @@ const Tools: React.FC<Props> = ({ isLastMessage }) => {
</div> </div>
); );
if (isMobile) {
return (
<div className={`${prefixCls}-mobile-tools`}>
{isLastMessage && <div className={`${prefixCls}-tools`}>{lockDomainSection}</div>}
{feedbackSection}
</div>
);
}
return ( return (
<div className={prefixCls}> <div className={prefixCls}>
<Button shape="round" onClick={changeChart}> {!isMobile && !isMobileMode && (
<>
</Button> <Button shape="round" onClick={changeChart}>
<Button shape="round" onClick={addToDashboard}>
</Button>
</Button> <Button shape="round" onClick={addToDashboard}>
{lockDomainSection}
{feedbackSection} </Button>
{feedbackSection}
</>
)}
</div> </div>
); );
}; };

View File

@@ -1,24 +1,44 @@
import { Input } from 'antd'; import { Input } from 'antd';
import styles from './style.module.less'; import styles from './style.module.less';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import ChatItem from '../components/ChatItem'; import ChatItem from '../components/ChatItem';
import { queryContext, searchRecommend } from '../service'; import { queryContext, searchRecommend } from '../service';
const { Search } = Input; const { Search } = Input;
const Chat = () => { const Chat = () => {
const [data, setData] = useState<any>();
const [inputMsg, setInputMsg] = useState(''); const [inputMsg, setInputMsg] = useState('');
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const [followQuestions, setFollowQuestions] = useState<string[]>([]);
const [triggerResize, setTriggerResize] = useState(false);
const onWindowResize = () => {
setTriggerResize(true);
setTimeout(() => {
setTriggerResize(false);
}, 0);
};
useEffect(() => {
window.addEventListener('resize', onWindowResize);
return () => {
window.removeEventListener('resize', onWindowResize);
};
}, []);
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target; const { value } = e.target;
setInputMsg(value); setInputMsg(value);
searchRecommend(value);
}; };
const onSearch = () => { const onSearch = () => {
setMsg(inputMsg); setMsg(inputMsg);
queryContext(inputMsg); };
const onMsgDataLoaded = (msgData: any) => {
setData(msgData);
setFollowQuestions(['测试1234测试', '测试1234测试', '测试1234测试']);
}; };
return ( return (
@@ -32,7 +52,15 @@ const Chat = () => {
/> />
</div> </div>
<div className={styles.chatItem}> <div className={styles.chatItem}>
<ChatItem msg={msg} suggestionEnable isLastMessage /> <ChatItem
msg={msg}
msgData={data}
onMsgDataLoaded={onMsgDataLoaded}
followQuestions={followQuestions}
isLastMessage
isMobileMode
triggerResize={triggerResize}
/>
</div> </div>
</div> </div>
); );

View File

@@ -2,7 +2,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
row-gap: 20px; row-gap: 20px;
padding: 30px; padding: 20px;
background: background:
linear-gradient(180deg,rgba(23,74,228,0) 29.44%,rgba(23,74,228,.06)),linear-gradient(90deg,#f3f3f7,#f3f3f7 20%,#ebf0f9 60%,#f3f3f7 80%,#f3f3f7); linear-gradient(180deg,rgba(23,74,228,0) 29.44%,rgba(23,74,228,.06)),linear-gradient(90deg,#f3f3f7,#f3f3f7 20%,#ebf0f9 60%,#f3f3f7 80%,#f3f3f7);
height: 100vh; height: 100vh;

View File

@@ -25,6 +25,7 @@ export type {
ChatContextType, ChatContextType,
MsgValidTypeEnum, MsgValidTypeEnum,
MsgDataType, MsgDataType,
InstructionResonseType,
ColumnType, ColumnType,
SuggestionItemType, SuggestionItemType,
SuggestionType, SuggestionType,

View File

@@ -19,7 +19,7 @@ axiosInstance.interceptors.request.use(
(config: any) => { (config: any) => {
const token = getToken(); const token = getToken();
if (token && config?.headers) { if (token && config?.headers) {
config.headers.auth = `Bearer ${token}`; config.headers.Auth = `Bearer ${token}`;
} }
return config; return config;
}, },
@@ -32,9 +32,16 @@ axiosInstance.interceptors.request.use(
// 响应拦截器 // 响应拦截器
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
(response: any) => { (response: any) => {
if (Number(response.data.code) === 403) { const redirect = response.headers.get('redirect');
window.location.href = '/#/login'; if (redirect === 'REDIRECT') {
return response; let win: any = window;
while (win !== win.top) {
win = win.top;
}
const contextpath = response.headers.get('contextpath');
win.location.href =
contextpath?.substring(0, contextpath?.indexOf('&')) +
`&redirect_uri=${encodeURIComponent(`http://${win.location.host}`)}`;
} }
return response; return response;
}, },

View File

@@ -2,7 +2,7 @@ import axios from './axiosInstance';
import { ChatContextType, HistoryType, MsgDataType, SearchRecommendItem } from '../common/type'; import { ChatContextType, HistoryType, MsgDataType, SearchRecommendItem } from '../common/type';
import { QueryDataType } from '../common/type'; import { QueryDataType } from '../common/type';
const DEFAULT_CHAT_ID = 2; const DEFAULT_CHAT_ID = 0;
const prefix = '/api'; const prefix = '/api';
@@ -57,7 +57,7 @@ export function getRelatedDimensionFromStatInfo(data: any) {
export function getMetricQueryInfo(data: any) { export function getMetricQueryInfo(data: any) {
return axios.get<any>( return axios.get<any>(
`getMetricQueryInfo/${data.classId}/${data.metricName}` `/openapi/bd-bi/api/polaris/intelligentQuery/getMetricQueryInfo/${data.classId}/${data.metricName}`
); );
} }
@@ -68,3 +68,10 @@ export function saveConversation(chatName: string) {
export function getAllConversations() { export function getAllConversations() {
return axios.get<Result<any>>(`${prefix}/chat/manage/getAll`); return axios.get<Result<any>>(`${prefix}/chat/manage/getAll`);
} }
export function queryEntities(entityId: string | number, domainId: number) {
return axios.post<Result<any>>(`${prefix}/chat/query/choice`, {
entityId,
domainId,
});
}

View File

@@ -26,7 +26,7 @@
&-metric { &-metric {
&::after { &::after {
background: #31c462; background: var(--primary-green);
} }
} }

View File

@@ -27,3 +27,4 @@
@import "../components/Tools/style.less"; @import "../components/Tools/style.less";
@import "../components/Suggestion/style.less"; @import "../components/Suggestion/style.less";

View File

@@ -50,7 +50,7 @@
--link-active-color: #2748d9; --link-active-color: #2748d9;
--link-bg-color: rgba(58, 100, 255, 0.1); --link-bg-color: rgba(58, 100, 255, 0.1);
--text-accent-color: #3a64ff; --text-accent-color: #3a64ff;
--primary-green: #00b354; --primary-green: #31c462;
--link-hover-bg-color: rgba(58, 100, 255, 0.06); --link-hover-bg-color: rgba(58, 100, 255, 0.06);
--success-2: rgba(82, 196, 26, 0.2); --success-2: rgba(82, 196, 26, 0.2);
--success-pink: #ff8193; --success-pink: #ff8193;

View File

@@ -159,7 +159,6 @@ export function getChartLightenColor(col) {
export const isMobile = window.navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i); export const isMobile = window.navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
export function setToken(token: string) { export function setToken(token: string) {
localStorage.setItem('SUPERSONIC_CHAT_TOKEN', token); localStorage.setItem('SUPERSONIC_CHAT_TOKEN', token);
} }

View File

@@ -18,8 +18,8 @@ yarn-error.log
/coverage /coverage
.idea .idea
package-lock.json
yarn.lock yarn.lock
package-lock.json
*bak *bak
.vscode .vscode

View File

@@ -6,7 +6,7 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
npm run build npm run build:inner
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "build failed" echo "build failed"
exit 1 exit 1

View File

@@ -5,6 +5,7 @@ import themeSettings from './themeSettings';
import proxy from './proxy'; import proxy from './proxy';
import routes from './routes'; import routes from './routes';
import moment from 'moment'; import moment from 'moment';
import ENV_CONFIG from './envConfig';
const { REACT_APP_ENV, RUN_TYPE } = process.env; const { REACT_APP_ENV, RUN_TYPE } = process.env;
@@ -19,6 +20,7 @@ export default defineConfig({
API_BASE_URL: '/api/semantic/', // 直接在define中挂载裸露的全局变量还需要配置eslintts相关配置才能导致在使用中不会飘红冗余较高这里挂在进程环境下 API_BASE_URL: '/api/semantic/', // 直接在define中挂载裸露的全局变量还需要配置eslintts相关配置才能导致在使用中不会飘红冗余较高这里挂在进程环境下
CHAT_API_BASE_URL: '/api/chat/', CHAT_API_BASE_URL: '/api/chat/',
AUTH_API_BASE_URL: '/api/auth/', AUTH_API_BASE_URL: '/api/auth/',
...ENV_CONFIG,
}, },
}, },
metas: [ metas: [

View File

@@ -13,8 +13,7 @@ const Settings: LayoutSettings & {
colorWeak: false, colorWeak: false,
title: '', title: '',
pwa: false, pwa: false,
// logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', iconfontUrl: '//at.alicdn.com/t/c/font_3201979_drwu4z3kkbi.js',
iconfontUrl: '//at.alicdn.com/t/c/font_3201979_rncj6jun6k.js',
splitMenus: true, splitMenus: true,
menu: { menu: {
defaultOpenAll: true, defaultOpenAll: true,

View File

@@ -0,0 +1,2 @@
const ENV_CONFIG = {};
export default ENV_CONFIG;

View File

@@ -65,7 +65,7 @@
"@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.1.0", "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",

View File

@@ -5,18 +5,12 @@ import { history } from 'umi';
import type { RunTimeLayoutConfig } from 'umi'; import type { RunTimeLayoutConfig } from 'umi';
import RightContent from '@/components/RightContent'; import RightContent from '@/components/RightContent';
import S2Icon, { ICON } from '@/components/S2Icon'; import S2Icon, { ICON } from '@/components/S2Icon';
import qs from 'qs';
import { queryCurrentUser } from './services/user'; import { queryCurrentUser } from './services/user';
import { queryToken } from './services/login';
import defaultSettings from '../config/defaultSettings'; import defaultSettings from '../config/defaultSettings';
import settings from '../config/themeSettings'; import settings from '../config/themeSettings';
import { deleteUrlQuery } from './utils/utils';
import { AUTH_TOKEN_KEY, FROM_URL_KEY } from '@/common/constants';
export { request } from './services/request'; export { request } from './services/request';
import { ROUTE_AUTH_CODES } from '../config/routes'; import { ROUTE_AUTH_CODES } from '../config/routes';
const TOKEN_KEY = AUTH_TOKEN_KEY;
const replaceRoute = '/'; const replaceRoute = '/';
const getRuningEnv = async () => { const getRuningEnv = async () => {
@@ -40,25 +34,6 @@ export const initialStateConfig = {
), ),
}; };
const getToken = async () => {
let { search } = window.location;
if (search.length > 0) {
search = search.slice(1);
}
const data = qs.parse(search);
if (data.code) {
try {
const fromUrl = localStorage.getItem(FROM_URL_KEY);
const res = await queryToken(data.code as string);
localStorage.setItem(TOKEN_KEY, res.payload);
const newUrl = deleteUrlQuery(window.location.href, 'code');
window.location.href = fromUrl || newUrl;
} catch (err) {
console.log(err);
}
}
};
const getAuthCodes = () => { const getAuthCodes = () => {
const { RUN_TYPE, APP_TARGET } = process.env; const { RUN_TYPE, APP_TARGET } = process.env;
if (RUN_TYPE === 'local') { if (RUN_TYPE === 'local') {
@@ -89,12 +64,6 @@ export async function getInitialState(): Promise<{
} catch (error) {} } catch (error) {}
return undefined; return undefined;
}; };
const { query } = history.location as any;
const currentToken = query[TOKEN_KEY] || localStorage.getItem(TOKEN_KEY);
if (window.location.host.includes('tmeoa') && !currentToken) {
await getToken();
}
const currentUser = await fetchUserInfo(); const currentUser = await fetchUserInfo();

View File

@@ -1,6 +1,6 @@
import IconFont from '@/components/IconFont'; import IconFont from '@/components/IconFont';
import { getTextWidth, groupByColumn, isMobile } from '@/utils/utils'; import { getTextWidth, groupByColumn } from '@/utils/utils';
import { AutoComplete, Select, Tag } from 'antd'; import { AutoComplete, Select, Tag, Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
@@ -8,13 +8,18 @@ import type { ForwardRefRenderFunction } from 'react';
import { searchRecommend } from 'supersonic-chat-sdk'; import { searchRecommend } from 'supersonic-chat-sdk';
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants'; import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
import styles from './style.less'; import styles from './style.less';
import { PLACE_HOLDER } from '@/common/constants'; import { PLACE_HOLDER } from '../constants';
import { DomainType } from '../type';
type Props = { type Props = {
inputMsg: string; inputMsg: string;
chatId?: number; chatId?: number;
currentDomain?: DomainType;
domains: DomainType[];
isMobileMode?: boolean;
onInputMsgChange: (value: string) => void; onInputMsgChange: (value: string) => void;
onSendMsg: (msg: string, domainId?: number) => void; onSendMsg: (msg: string, domainId?: number) => void;
onAddConversation: () => void;
}; };
const { OptGroup, Option } = Select; const { OptGroup, Option } = Select;
@@ -30,9 +35,19 @@ const compositionEndEvent = () => {
}; };
const ChatFooter: ForwardRefRenderFunction<any, Props> = ( const ChatFooter: ForwardRefRenderFunction<any, Props> = (
{ inputMsg, chatId, onInputMsgChange, onSendMsg }, {
inputMsg,
chatId,
currentDomain,
domains,
isMobileMode,
onInputMsgChange,
onSendMsg,
onAddConversation,
},
ref, ref,
) => { ) => {
const [domainOptions, setDomainOptions] = useState<DomainType[]>([]);
const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({}); const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({});
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
@@ -73,39 +88,61 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
}; };
}, []); }, []);
const getStepOptions = (recommends: any[]) => {
const data = groupByColumn(recommends, 'domainName');
return isMobileMode && recommends.length > 6
? Object.keys(data)
.slice(0, 4)
.reduce((result, key) => {
result[key] = data[key].slice(
0,
Object.keys(data).length > 2 ? 2 : Object.keys(data).length > 1 ? 3 : 6,
);
return result;
}, {})
: data;
};
const processMsg = (msg: string, domains: DomainType[]) => {
let msgValue = msg;
let domainId: number | undefined;
if (msg?.[0] === '@') {
const domain = domains.find((item) => msg.includes(`@${item.name}`));
msgValue = domain ? msg.replace(`@${domain.name}`, '') : msg;
domainId = domain?.id;
}
return { msgValue, domainId };
};
const debounceGetWordsFunc = useCallback(() => { const debounceGetWordsFunc = useCallback(() => {
const getAssociateWords = async (msg: string, chatId?: number) => { const getAssociateWords = async (
msg: string,
domains: DomainType[],
chatId?: number,
domain?: DomainType,
) => {
if (isPinyin) { if (isPinyin) {
return; return;
} }
if (msg === '' || (msg.length === 1 && msg[0] === '@')) {
return;
}
fetchRef.current += 1; fetchRef.current += 1;
const fetchId = fetchRef.current; const fetchId = fetchRef.current;
const res = await searchRecommend(msg, chatId); const { msgValue, domainId } = processMsg(msg, domains);
const res = await searchRecommend(msgValue.trim(), chatId, domainId || domain?.id);
if (fetchId !== fetchRef.current) { if (fetchId !== fetchRef.current) {
return; return;
} }
const recommends = msg ? res.data.data || [] : [];
const recommends = msgValue ? res.data.data || [] : [];
const stepOptionList = recommends.map((item: any) => item.subRecommend); const stepOptionList = recommends.map((item: any) => item.subRecommend);
if (stepOptionList.length > 0 && stepOptionList.every((item: any) => item !== null)) { if (stepOptionList.length > 0 && stepOptionList.every((item: any) => item !== null)) {
const data = groupByColumn(recommends, 'domainName'); setStepOptions(getStepOptions(recommends));
const optionsData =
isMobile && recommends.length > 6
? Object.keys(data)
.slice(0, 4)
.reduce((result, key) => {
result[key] = data[key].slice(
0,
Object.keys(data).length > 2 ? 2 : Object.keys(data).length > 1 ? 3 : 6,
);
return result;
}, {})
: data;
setStepOptions(optionsData);
} else { } else {
setStepOptions({}); setStepOptions({});
} }
setOpen(recommends.length > 0); setOpen(recommends.length > 0);
}; };
return debounce(getAssociateWords, 20); return debounce(getAssociateWords, 20);
@@ -114,13 +151,27 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
const [debounceGetWords] = useState<any>(debounceGetWordsFunc); const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
useEffect(() => { useEffect(() => {
if (inputMsg.length === 1 && inputMsg[0] === '@') {
setOpen(true);
setDomainOptions(domains);
setStepOptions({});
return;
} else {
setOpen(false);
if (domainOptions.length > 0) {
setTimeout(() => {
setDomainOptions([]);
}, 500);
}
}
if (!isSelect) { if (!isSelect) {
debounceGetWords(inputMsg, chatId); debounceGetWords(inputMsg, domains, chatId, currentDomain);
} else { } else {
isSelect = false; isSelect = false;
} }
if (!inputMsg) { if (!inputMsg) {
setStepOptions({}); setStepOptions({});
fetchRef.current = 0;
} }
}, [inputMsg]); }, [inputMsg]);
@@ -140,6 +191,10 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
const textWidth = getTextWidth(inputMsg); const textWidth = getTextWidth(inputMsg);
if (Object.keys(stepOptions).length > 0) { if (Object.keys(stepOptions).length > 0) {
autoCompleteDropdown.style.marginLeft = `${textWidth}px`; autoCompleteDropdown.style.marginLeft = `${textWidth}px`;
} else {
setTimeout(() => {
autoCompleteDropdown.style.marginLeft = `0px`;
}, 200);
} }
}, [stepOptions]); }, [stepOptions]);
@@ -157,18 +212,20 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
if (option && isSelect) { if (option && isSelect) {
onSendMsg(option.recommend, option.domainId); onSendMsg(option.recommend, option.domainId);
} else { } else {
onSendMsg(value); onSendMsg(value.trim());
} }
}; };
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, { const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
[styles.external]: true, [styles.mobile]: isMobileMode,
[styles.mobile]: isMobile, [styles.domainOptions]: domainOptions.length > 0,
}); });
const onSelect = (value: string) => { const onSelect = (value: string) => {
isSelect = true; isSelect = true;
sendMsg(value); if (domainOptions.length === 0) {
sendMsg(value);
}
setOpen(false); setOpen(false);
setTimeout(() => { setTimeout(() => {
isSelect = false; isSelect = false;
@@ -176,20 +233,31 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
}; };
const chatFooterClass = classNames(styles.chatFooter, { const chatFooterClass = classNames(styles.chatFooter, {
[styles.mobile]: isMobile, [styles.mobile]: isMobileMode,
}); });
return ( return (
<div className={chatFooterClass}> <div className={chatFooterClass}>
<div className={styles.composer}> <div className={styles.composer}>
<Tooltip title="新建对话">
<IconFont
type="icon-icon-add-conversation-line"
className={styles.addConversation}
onClick={onAddConversation}
/>
</Tooltip>
<div className={styles.composerInputWrapper}> <div className={styles.composerInputWrapper}>
<AutoComplete <AutoComplete
className={styles.composerInput} className={styles.composerInput}
placeholder={PLACE_HOLDER} placeholder={
currentDomain
? `请输入【${currentDomain.name}】主题的问题,可使用@切换到其他主题`
: PLACE_HOLDER
}
value={inputMsg} value={inputMsg}
onChange={onInputMsgChange} onChange={onInputMsgChange}
onSelect={onSelect} onSelect={onSelect}
autoFocus={!isMobile} autoFocus={!isMobileMode}
backfill backfill
ref={inputRef} ref={inputRef}
id="chatInput" id="chatInput"
@@ -210,46 +278,68 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
listHeight={500} listHeight={500}
allowClear allowClear
open={open} open={open}
getPopupContainer={isMobile ? (triggerNode) => triggerNode.parentNode : undefined} getPopupContainer={(triggerNode) => triggerNode.parentNode}
> >
{Object.keys(stepOptions).map((key) => { {domainOptions.length > 0
return ( ? domainOptions.map((domain) => {
<OptGroup key={key} label={key}> return (
{stepOptions[key].map((option) => (
<Option <Option
key={`${option.recommend}${option.domainName ? `_${option.domainName}` : ''}`} key={domain.id}
value={ value={`@${domain.name} `}
Object.keys(stepOptions).length === 1
? option.recommend
: `${option.domainName || ''}${option.recommend}`
}
className={styles.searchOption} className={styles.searchOption}
> >
<div className={styles.optionContent}> {domain.name}
{option.schemaElementType && (
<Tag
className={styles.semanticType}
color={
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
option.schemaElementType === SemanticTypeEnum.DOMAIN
? 'blue'
: option.schemaElementType === SemanticTypeEnum.VALUE
? 'geekblue'
: 'orange'
}
>
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
option.schemaElementType ||
'维度'}
</Tag>
)}
{option.subRecommend}
</div>
</Option> </Option>
))} );
</OptGroup> })
); : Object.keys(stepOptions).map((key) => {
})} return (
<OptGroup key={key} label={key}>
{stepOptions[key].map((option) => {
let optionValue =
Object.keys(stepOptions).length === 1
? option.recommend
: `${option.domainName || ''}${option.recommend}`;
if (inputMsg[0] === '@') {
const domain = domains.find((item) => inputMsg.includes(item.name));
optionValue = domain
? `@${domain.name} ${option.recommend}`
: optionValue;
}
return (
<Option
key={`${option.recommend}${
option.domainName ? `_${option.domainName}` : ''
}`}
value={optionValue}
className={styles.searchOption}
>
<div className={styles.optionContent}>
{option.schemaElementType && (
<Tag
className={styles.semanticType}
color={
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
option.schemaElementType === SemanticTypeEnum.DOMAIN
? 'blue'
: option.schemaElementType === SemanticTypeEnum.VALUE
? 'geekblue'
: 'orange'
}
>
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
option.schemaElementType ||
'维度'}
</Tag>
)}
{option.subRecommend}
</div>
</Option>
);
})}
</OptGroup>
);
})}
</AutoComplete> </AutoComplete>
<div <div
className={classNames(styles.sendBtn, { className={classNames(styles.sendBtn, {

View File

@@ -11,6 +11,32 @@
display: flex; display: flex;
height: 46px; height: 46px;
.collapseBtn {
height: 46px;
margin: 0 10px;
color: var(--text-color-third);
font-size: 20px;
line-height: 46px;
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
.addConversation {
height: 46px;
margin: 0 20px 0 10px;
color: var(--text-color-fourth);
font-size: 26px;
line-height: 54px;
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
.composerInputWrapper { .composerInputWrapper {
flex: 1; flex: 1;
@@ -28,7 +54,7 @@
background: #fff; background: #fff;
border: 0; border: 0;
border-radius: 24px; border-radius: 24px;
box-shadow: rgba(0, 0, 0, 0.07) 0 -0.5px 0, rgba(0, 0, 0, 0.1) 0 0 18px; box-shadow: rgba(0, 0, 0, 0.07) 0px -0.5px 0px, rgba(0, 0, 0, 0.1) 0px 0px 18px;
transition: border-color 0.15s ease-in-out; transition: border-color 0.15s ease-in-out;
resize: none; resize: none;
@@ -62,7 +88,7 @@
:global { :global {
.ant-select-focused { .ant-select-focused {
.ant-select-selector { .ant-select-selector {
box-shadow: rgb(74, 114, 245) 0 0 3px !important; box-shadow: rgb(74, 114, 245) 0px 0px 3px !important;
} }
} }
} }
@@ -96,6 +122,11 @@
margin: 12px; margin: 12px;
margin-bottom: 20px; margin-bottom: 20px;
.addConversation {
height: 40px;
margin: 0 12px 0 4px;
}
.composer { .composer {
height: 40px; height: 40px;
@@ -134,17 +165,26 @@
} }
.autoCompleteDropdown { .autoCompleteDropdown {
left: 285px !important; left: 20px !important;
width: fit-content !important; width: fit-content !important;
min-width: 50px !important; min-width: 100px !important;
border-radius: 6px; border-radius: 6px;
&.external { &.domainOptions {
left: 226px !important; width: 150px !important;
}
&.mobile { .searchOption {
left: 20px !important; padding: 0 10px;
color: var(--text-color-secondary);
font-size: 14px;
}
:global {
.ant-select-item {
height: 30px !important;
line-height: 30px !important;
}
}
} }
} }

View File

@@ -1,20 +1,23 @@
import Text from './components/Text'; import Text from './components/Text';
import { memo, useCallback, useEffect } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import styles from './style.less';
import { connect, Dispatch } from 'umi';
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';
type Props = { type Props = {
id: string; id: string;
chatId: number; chatId: number;
messageList: MessageItem[]; messageList: MessageItem[];
dispatch: Dispatch; miniProgramLoading: boolean;
isMobileMode?: boolean;
onClickMessageContainer: () => void; onClickMessageContainer: () => void;
onMsgDataLoaded: (data: MsgDataType) => void; onMsgDataLoaded: (data: MsgDataType, questionId: string | number) => void;
onSelectSuggestion: (value: string) => void; onSelectSuggestion: (value: string) => void;
onCheckMore: (data: MsgDataType) => void;
onUpdateMessageScroll: () => void; onUpdateMessageScroll: () => void;
}; };
@@ -22,52 +25,92 @@ const MessageContainer: React.FC<Props> = ({
id, id,
chatId, chatId,
messageList, messageList,
dispatch, miniProgramLoading,
isMobileMode,
onClickMessageContainer, onClickMessageContainer,
onMsgDataLoaded, onMsgDataLoaded,
onSelectSuggestion, onSelectSuggestion,
onUpdateMessageScroll, onUpdateMessageScroll,
}) => { }) => {
const onWindowResize = useCallback(() => { const [triggerResize, setTriggerResize] = useState(false);
dispatch({
type: 'windowResize/setTriggerResize', const onResize = useCallback(() => {
payload: true, setTriggerResize(true);
});
setTimeout(() => { setTimeout(() => {
dispatch({ setTriggerResize(false);
type: 'windowResize/setTriggerResize',
payload: false,
});
}, 0); }, 0);
}, []); }, []);
useEffect(() => { useEffect(() => {
window.addEventListener('resize', onWindowResize); window.addEventListener('resize', onResize);
return () => { return () => {
window.removeEventListener('resize', onWindowResize); window.removeEventListener('resize', onResize);
}; };
}, []); }, []);
const messageListClass = classNames(styles.messageList, {
[styles.miniProgramLoading]: miniProgramLoading,
});
const getFollowQuestions = (index: number) => {
const followQuestions: string[] = [];
const currentMsg = messageList[index];
const currentMsgData = currentMsg.msgData;
const msgs = messageList.slice(0, index).reverse();
for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i];
const msgDomainId = msg.msgData?.chatContext?.domainId;
const msgEntityId = msg.msgData?.entityInfo?.entityId;
const currentMsgDomainId = currentMsgData?.chatContext?.domainId;
const currentMsgEntityId = currentMsgData?.entityInfo?.entityId;
if (
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.INSTRUCTION) &&
!!currentMsgDomainId &&
!!currentMsgEntityId &&
msgDomainId === currentMsgDomainId &&
msgEntityId === currentMsgEntityId &&
msg.msg
) {
followQuestions.push(msg.msg);
} else {
break;
}
}
return followQuestions;
};
return ( return (
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}> <div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
<div className={styles.messageList}> {miniProgramLoading && <Skeleton className={styles.messageLoading} paragraph={{ rows: 5 }} />}
<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 followQuestions = getFollowQuestions(index);
return ( return (
<div key={`${msgItem.id}`} id={`${msgItem.id}`} className={styles.messageItem}> <div key={msgId} id={`${msgId}`} className={styles.messageItem}>
{msgItem.type === MessageTypeEnum.TEXT && <Text position="left" data={msgItem.msg} />} {type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
{msgItem.type === MessageTypeEnum.QUESTION && ( {type === MessageTypeEnum.QUESTION && (
<> <>
<Text position="right" data={msgItem.msg} quote={msgItem.quote} /> <Text position="right" data={msg} />
{identityMsg && <Text position="left" data={identityMsg} />}
<ChatItem <ChatItem
msg={msgItem.msg || ''} msg={msgValue || msg || ''}
msgData={msgItem.msgData} followQuestions={followQuestions}
msgData={msgData}
conversationId={chatId} conversationId={chatId}
classId={msgItem.domainId} domainId={domainId}
isLastMessage={index === messageList.length - 1} isLastMessage={index === messageList.length - 1}
onLastMsgDataLoaded={onMsgDataLoaded} isMobileMode={isMobileMode}
triggerResize={triggerResize}
onMsgDataLoaded={(data: MsgDataType) => {
onMsgDataLoaded(data, msgId);
}}
onSelectSuggestion={onSelectSuggestion} onSelectSuggestion={onSelectSuggestion}
onUpdateMessageScroll={onUpdateMessageScroll} onUpdateMessageScroll={onUpdateMessageScroll}
suggestionEnable
/> />
</> </>
)} )}
@@ -80,10 +123,14 @@ const MessageContainer: React.FC<Props> = ({
}; };
function areEqual(prevProps: Props, nextProps: Props) { function areEqual(prevProps: Props, nextProps: Props) {
if (prevProps.id === nextProps.id && isEqual(prevProps.messageList, nextProps.messageList)) { if (
prevProps.id === nextProps.id &&
isEqual(prevProps.messageList, nextProps.messageList) &&
prevProps.miniProgramLoading === nextProps.miniProgramLoading
) {
return true; return true;
} }
return false; return false;
} }
export default connect()(memo(MessageContainer, areEqual)); export default memo(MessageContainer, areEqual);

View File

@@ -0,0 +1,22 @@
import { DomainType } from '../../type';
import styles from './style.less';
type Props = {
domain: DomainType;
};
const DomainInfo: React.FC<Props> = ({ domain }) => {
return (
<div className={styles.context}>
<div className={styles.title}></div>
<div className={styles.content}>
<div className={styles.field}>
<span className={styles.fieldName}></span>
<span className={styles.fieldValue}>{domain.name}</span>
</div>
</div>
</div>
);
};
export default DomainInfo;

View File

@@ -1,13 +1,14 @@
import moment from 'moment'; import moment from 'moment';
import styles from './style.less'; import styles from './style.less';
import type { ChatContextType } from 'supersonic-chat-sdk'; import type { ChatContextType, EntityInfoType } from 'supersonic-chat-sdk';
type Props = { type Props = {
chatContext: ChatContextType; chatContext: ChatContextType;
entityInfo?: EntityInfoType;
}; };
const Context: React.FC<Props> = ({ chatContext }) => { const Context: React.FC<Props> = ({ chatContext, entityInfo }) => {
const { domainName, metrics, dateInfo, filters } = chatContext; const { domainName, metrics, dateInfo, dimensionFilters } = chatContext;
return ( return (
<div className={styles.context}> <div className={styles.context}>
@@ -17,17 +18,15 @@ const Context: React.FC<Props> = ({ chatContext }) => {
<span className={styles.fieldName}></span> <span className={styles.fieldName}></span>
<span className={styles.fieldValue}>{domainName}</span> <span className={styles.fieldValue}>{domainName}</span>
</div> </div>
{ {dateInfo && (
dateInfo && ( <div className={styles.field}>
<div className={styles.field}> <span className={styles.fieldName}></span>
<span className={styles.fieldName}></span> <span className={styles.fieldValue}>
<span className={styles.fieldValue}> {dateInfo.text ||
{dateInfo.text || `${moment(dateInfo.endDate).diff(moment(dateInfo.startDate), 'days') + 1}`}
`${moment(dateInfo.endDate).diff(moment(dateInfo.startDate), 'days') + 1}`} </span>
</span> </div>
</div> )}
)
}
{metrics && metrics.length > 0 && ( {metrics && metrics.length > 0 && (
<div className={styles.field}> <div className={styles.field}>
<span className={styles.fieldName}></span> <span className={styles.fieldName}></span>
@@ -36,20 +35,22 @@ const Context: React.FC<Props> = ({ chatContext }) => {
</span> </span>
</div> </div>
)} )}
{filters && filters.length > 0 && ( {dimensionFilters &&
<div className={styles.filterSection}> dimensionFilters.length > 0 &&
<div className={styles.fieldName}></div> !(entityInfo?.dimensions && entityInfo.dimensions.length > 0) && (
<div className={styles.filterValues}> <div className={styles.filterSection}>
{filters.map((filter) => { <div className={styles.fieldName}></div>
return ( <div className={styles.filterValues}>
<div className={styles.filterItem} key={filter.name}> {dimensionFilters.map((filter) => {
{filter.name}{filter.value} return (
</div> <div className={styles.filterItem} key={filter.name}>
); {filter.name}{filter.value}
})} </div>
);
})}
</div>
</div> </div>
</div> )}
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,6 +1,8 @@
.context { .context {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px 10px 0;
border-top: 1px solid #ccc;
.title { .title {
margin-bottom: 22px; margin-bottom: 22px;
@@ -45,11 +47,11 @@
} }
.fieldValue { .fieldValue {
max-width: 150px;
overflow: hidden;
color: var(--text-color); color: var(--text-color);
white-space: nowrap;
&.switchField { text-overflow: ellipsis;
cursor: pointer;
}
} }
.filterValues { .filterValues {

View File

@@ -1,6 +1,6 @@
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import moment from 'moment'; import moment from 'moment';
import type { ConversationDetailType } from '../../type'; import type { ConversationDetailType } from '../../../type';
import styles from './style.less'; import styles from './style.less';
type Props = { type Props = {

View File

@@ -5,8 +5,8 @@
z-index: 10; z-index: 10;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 215px; width: 100%;
height: calc(100vh - 48px); height: calc(100vh - 78px);
overflow: hidden; overflow: hidden;
background: #f3f3f7; background: #f3f3f7;
border-right: 1px solid var(--border-color-base); border-right: 1px solid var(--border-color-base);

View File

@@ -1,7 +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';
const FormItem = Form.Item; const FormItem = Form.Item;
@@ -43,7 +44,7 @@ const ConversationModal: React.FC<Props> = ({ visible, editConversation, onClose
return ( return (
<Modal <Modal
title="修改问答对话名称" title={`修改${CHAT_TITLE}问答名称`}
visible={visible} visible={visible}
onCancel={onClose} onCancel={onClose}
onOk={onConfirm} onOk={onConfirm}
@@ -52,7 +53,7 @@ const ConversationModal: React.FC<Props> = ({ visible, editConversation, onClose
<Form {...layout} form={form}> <Form {...layout} form={form}>
<FormItem name="conversationName" label="名称" rules={[{ required: true }]}> <FormItem name="conversationName" label="名称" rules={[{ required: true }]}>
<Input <Input
placeholder="请输入问答对话名称" placeholder={`请输入${CHAT_TITLE}问答名称`}
ref={conversationNameInputRef} ref={conversationNameInputRef}
onPressEnter={onConfirm} onPressEnter={onConfirm}
/> />

View File

@@ -1,5 +1,5 @@
import IconFont from '@/components/IconFont'; import IconFont from '@/components/IconFont';
import { Dropdown, Menu, message } from 'antd'; import { Dropdown, Menu } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
useEffect, useEffect,
@@ -9,11 +9,12 @@ import {
useImperativeHandle, useImperativeHandle,
} from 'react'; } from 'react';
import { useLocation } from 'umi'; import { useLocation } from 'umi';
import ConversationHistory from './components/ConversationHistory'; import ConversationHistory from './ConversationHistory';
import ConversationModal from './components/ConversationModal'; import ConversationModal from './ConversationModal';
import { deleteConversation, getAllConversations, saveConversation } from './service'; import { deleteConversation, getAllConversations, saveConversation } from '../../service';
import styles from './style.less'; import styles from './style.less';
import { ConversationDetailType } from './type'; import { ConversationDetailType } from '../../type';
import { DEFAULT_CONVERSATION_NAME } from '../../constants';
type Props = { type Props = {
currentConversation?: ConversationDetailType; currentConversation?: ConversationDetailType;
@@ -65,7 +66,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
}; };
useEffect(() => { useEffect(() => {
if (q && cid === undefined) { if (q && cid === undefined && location.pathname === '/workbench/chat') {
onAddConversation(q); onAddConversation(q);
} else { } else {
initData(); initData();
@@ -73,7 +74,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
}, [q]); }, [q]);
const addConversation = async (name?: string) => { const addConversation = async (name?: string) => {
await saveConversation(name || '新问答对话'); await saveConversation(name || DEFAULT_CONVERSATION_NAME);
return updateData(); return updateData();
}; };
@@ -96,21 +97,14 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
} }
}; };
const onNewChat = () => {
onAddConversation('新问答对话');
};
const onShowHistory = () => { const onShowHistory = () => {
setHistoryVisible(true); setHistoryVisible(true);
}; };
const onShare = () => {
message.info('正在开发中,敬请期待');
};
return ( return (
<div className={styles.conversation}> <div className={styles.conversation}>
<div className={styles.leftSection}> <div className={styles.conversationSection}>
<div className={styles.sectionTitle}></div>
<div className={styles.conversationList}> <div className={styles.conversationList}>
{conversations.map((item) => { {conversations.map((item) => {
const conversationItemClass = classNames(styles.conversationItem, { const conversationItemClass = classNames(styles.conversationItem, {
@@ -133,7 +127,6 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
trigger={['contextMenu']} trigger={['contextMenu']}
> >
<div <div
key={item.chatId}
className={conversationItemClass} className={conversationItemClass}
onClick={() => { onClick={() => {
onSelectConversation(item); onSelectConversation(item);
@@ -159,19 +152,6 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
</div> </div>
</div> </div>
</div> </div>
<div className={styles.operateSection}>
<div className={styles.operateItem} onClick={onNewChat}>
<IconFont type="icon-add" className={`${styles.operateIcon} ${styles.addIcon}`} />
<div className={styles.operateLabel}></div>
</div>
<div className={styles.operateItem} onClick={onShare}>
<IconFont
type="icon-fenxiang2"
className={`${styles.operateIcon} ${styles.shareIcon}`}
/>
<div className={styles.operateLabel}></div>
</div>
</div>
</div> </div>
{historyVisible && ( {historyVisible && (
<ConversationHistory <ConversationHistory

View File

@@ -0,0 +1,50 @@
.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);
}
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
import classNames from 'classnames';
import { DomainType } from '../../type';
import styles from './style.less';
type Props = {
domains: DomainType[];
currentDomain?: DomainType;
onSelectDomain: (domain: DomainType) => void;
};
const Domains: React.FC<Props> = ({ domains, currentDomain, onSelectDomain }) => {
return (
<div className={styles.domains}>
<div className={styles.titleBar}>
<div className={styles.title}></div>
<div className={styles.subTitle}>(@)</div>
</div>
<div className={styles.domainList}>
{domains
.filter((domain) => domain.id !== -1)
.map((domain) => {
const domainItemClass = classNames(styles.domainItem, {
[styles.activeDomainItem]: currentDomain?.id === domain.id,
});
return (
<div key={domain.id}>
<div
className={domainItemClass}
onClick={() => {
onSelectDomain(domain);
}}
>
{/* <IconFont type="icon-yinleku" className={styles.domainIcon} /> */}
<div className={styles.domainName}>{domain.name}</div>
</div>
</div>
);
})}
</div>
</div>
);
};
export default Domains;

View File

@@ -0,0 +1,70 @@
.domains {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ccc;
.titleBar {
display: flex;
align-items: center;
column-gap: 4px;
margin-bottom: 12px;
.title {
padding-left: 10px;
color: var(--text-color);
font-size: 16px;
line-height: 24px;
}
.subTitle {
font-size: 13px;
color: var(--text-color-third);
}
}
.domainList {
display: flex;
flex-direction: column;
.domainItem {
display: flex;
align-items: center;
padding: 4px 10px;
font-size: 14px;
cursor: pointer;
.loadingIcon {
margin-right: 6px;
color: var(--text-color-fifth);
font-size: 12px;
}
.arrowIcon {
margin-right: 6px;
color: var(--text-color-fifth);
font-size: 12px;
}
.domainIcon {
margin-right: 6px;
color: var(--blue);
}
.domainName {
width: 150px;
overflow: hidden;
color: var(--text-color-secondary);
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
background-color: var(--link-hover-bg-color);
}
&.activeDomainItem {
background-color: var(--link-hover-bg-color);
}
}
}
}

View File

@@ -20,11 +20,15 @@ const Introduction: React.FC<Props> = ({ currentEntity }) => {
return ( return (
<div className={styles.field} key={dimension.name}> <div className={styles.field} key={dimension.name}>
<span className={styles.fieldName}>{dimension.name}</span> <span className={styles.fieldName}>{dimension.name}</span>
<span className={styles.fieldValue}> {dimension.bizName.includes('photo') ? (
{dimension.bizName.includes('publish_time') <img width={40} height={40} src={dimension.value} alt="" />
? moment(dimension.value).format('YYYY-MM-DD') ) : (
: dimension.value} <span className={styles.fieldValue}>
</span> {dimension.bizName.includes('publish_time')
? moment(dimension.value).format('YYYY-MM-DD')
: dimension.value}
</span>
)}
</div> </div>
); );
})} })}

View File

@@ -1,7 +1,7 @@
.introduction { .introduction {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-bottom: 4px; padding: 0 10px 4px;
.title { .title {
margin-bottom: 22px; margin-bottom: 22px;

View File

@@ -3,24 +3,55 @@ import Context from './Context';
import Introduction from './Introduction'; import Introduction from './Introduction';
import styles from './style.less'; import styles from './style.less';
import type { MsgDataType } from 'supersonic-chat-sdk'; import type { MsgDataType } from 'supersonic-chat-sdk';
import Domains from './Domains';
import { ConversationDetailType, DomainType } from '../type';
import DomainInfo from './Context/DomainInfo';
import Conversation from './Conversation';
type Props = { type Props = {
domains: DomainType[];
currentEntity?: MsgDataType; currentEntity?: MsgDataType;
currentConversation?: ConversationDetailType;
currentDomain?: DomainType;
conversationRef: any;
onSelectConversation: (conversation: ConversationDetailType, name?: string) => void;
onSelectDomain: (domain: DomainType) => void;
}; };
const RightSection: React.FC<Props> = ({ currentEntity }) => { const RightSection: React.FC<Props> = ({
domains,
currentEntity,
currentDomain,
currentConversation,
conversationRef,
onSelectConversation,
onSelectDomain,
}) => {
const rightSectionClass = classNames(styles.rightSection, { const rightSectionClass = classNames(styles.rightSection, {
[styles.external]: true, [styles.external]: false,
}); });
return ( return (
<div className={rightSectionClass}> <div className={rightSectionClass}>
{currentEntity && ( <Conversation
currentConversation={currentConversation}
onSelectConversation={onSelectConversation}
ref={conversationRef}
/>
{currentDomain && !currentEntity && (
<div className={styles.entityInfo}> <div className={styles.entityInfo}>
{currentEntity?.chatContext && <Context chatContext={currentEntity.chatContext} />} <DomainInfo domain={currentDomain} />
</div>
)}
{!!currentEntity?.chatContext?.domainId && (
<div className={styles.entityInfo}>
<Context chatContext={currentEntity.chatContext} entityInfo={currentEntity.entityInfo} />
<Introduction currentEntity={currentEntity} /> <Introduction currentEntity={currentEntity} />
</div> </div>
)} )}
{domains && domains.length > 0 && (
<Domains domains={domains} currentDomain={currentDomain} onSelectDomain={onSelectDomain} />
)}
</div> </div>
); );
}; };

View File

@@ -1,13 +1,12 @@
.rightSection { .rightSection {
width: 225px; width: 225px;
height: calc(100vh - 48px); height: calc(100vh - 48px);
padding-right: 10px; margin-right: 12px;
padding-bottom: 10px; padding-bottom: 10px;
padding-left: 20px;
overflow-y: auto; overflow-y: auto;
.entityInfo { .entityInfo {
margin-top: 30px; margin-top: 20px;
.topInfo { .topInfo {
margin-bottom: 20px; margin-bottom: 20px;

View File

@@ -0,0 +1,8 @@
import IconFont from '@/components/IconFont';
import styles from './style.less';
const LeftAvatar = () => {
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
};
export default LeftAvatar;

View File

@@ -0,0 +1,13 @@
.leftAvatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-right: 6px;
margin-right: 6px;
color: var(--chat-blue);
font-size: 40px;
background-color: #fff;
border-radius: 50%;
}

View File

@@ -3,27 +3,52 @@ import styles from './style.less';
type Props = { type Props = {
position: 'left' | 'right'; position: 'left' | 'right';
width?: number | string;
height?: number | string;
bubbleClassName?: string; bubbleClassName?: string;
aggregator?: string; domainName?: string;
noTime?: boolean; question?: string;
followQuestions?: string[];
}; };
const Message: React.FC<Props> = ({ position, children, bubbleClassName }) => { const Message: React.FC<Props> = ({
position,
width,
height,
children,
bubbleClassName,
domainName,
question,
followQuestions,
}) => {
const messageClass = classNames(styles.message, { const messageClass = classNames(styles.message, {
[styles.left]: position === 'left', [styles.left]: position === 'left',
[styles.right]: position === 'right', [styles.right]: position === 'right',
}); });
const leftTitle = question
? followQuestions && followQuestions.length > 0
? `多轮对话:${[question, ...followQuestions].join(' ← ')}`
: `单轮对话:${question}`
: '';
return ( return (
<div className={messageClass}> <div className={messageClass} style={{ width }}>
{!!domainName && <div className={styles.domainName}>{domainName}</div>}
<div className={styles.messageContent}> <div className={styles.messageContent}>
<div className={styles.messageBody}> <div className={styles.messageBody}>
<div <div
className={`${styles.bubble}${bubbleClassName ? ` ${bubbleClassName}` : ''}`} className={`${styles.bubble}${bubbleClassName ? ` ${bubbleClassName}` : ''}`}
style={{ height }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
{position === 'left' && question && (
<div className={styles.messageTopBar} title={leftTitle}>
{leftTitle}
</div>
)}
{children} {children}
</div> </div>
</div> </div>

View File

@@ -1,3 +1,5 @@
import classNames from 'classnames';
import LeftAvatar from './LeftAvatar';
import Message from './Message'; import Message from './Message';
import styles from './style.less'; import styles from './style.less';
@@ -8,11 +10,17 @@ type Props = {
}; };
const Text: React.FC<Props> = ({ position, data, quote }) => { const Text: React.FC<Props> = ({ position, data, quote }) => {
const textWrapperClass = classNames(styles.textWrapper, {
[styles.rightTextWrapper]: position === 'right',
});
return ( return (
<Message position={position} bubbleClassName={styles.textBubble}> <div className={textWrapperClass}>
{position === 'right' && quote && <div className={styles.quote}>{quote}</div>} {position === 'left' && <LeftAvatar />}
<div className={styles.text}>{data}</div> <Message position={position} bubbleClassName={styles.textBubble}>
</Message> {position === 'right' && quote && <div className={styles.quote}>{quote}</div>}
<div className={styles.text}>{data}</div>
</Message>
</div>
); );
}; };

View File

@@ -1,10 +1,28 @@
.message { .message {
.domainName {
margin-bottom: 2px;
margin-left: 4px;
color: var(--text-color);
font-weight: 500;
}
.messageContent { .messageContent {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
.messageBody { .messageBody {
width: 100%; width: 100%;
.messageTopBar {
max-width: 90%;
margin: 0 16px;
padding: 12px 0 8px;
overflow: hidden;
color: var(--text-color-third);
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
background-color: #fff;
}
} }
.avatar { .avatar {
@@ -73,7 +91,6 @@
font-size: 16px; font-size: 16px;
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%); background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 12px 4px 12px 12px;
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);
.text { .text {
@@ -275,3 +292,16 @@
} }
} }
} }
.textWrapper {
display: flex;
align-items: center;
&.rightTextWrapper {
justify-content: flex-end;
}
.rightAvatar {
margin-left: 6px;
}
}

View File

@@ -26,3 +26,11 @@ export const SEMANTIC_TYPE_MAP = {
[SemanticTypeEnum.METRIC]: '指标', [SemanticTypeEnum.METRIC]: '指标',
[SemanticTypeEnum.VALUE]: '维度值', [SemanticTypeEnum.VALUE]: '维度值',
}; };
export const DEFAULT_CONVERSATION_NAME = '新问答对话'
export const WEB_TITLE = '问答对话'
export const CHAT_TITLE = '问答'
export const PLACE_HOLDER = '请输入您的问题'

View File

@@ -1,27 +1,28 @@
import { updateMessageContainerScroll, isMobile, uuid } from '@/utils/utils'; import { updateMessageContainerScroll, isMobile, uuid, getLeafList } from '@/utils/utils';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Helmet } from 'umi'; import { Helmet } from 'umi';
import MessageContainer from './MessageContainer'; import MessageContainer from './MessageContainer';
import styles from './style.less'; import styles from './style.less';
import { ConversationDetailType, MessageItem, MessageTypeEnum } from './type'; import { ConversationDetailType, DomainType, MessageItem, MessageTypeEnum } from './type';
import { updateConversationName } from './service'; import { getDomainList, updateConversationName } from './service';
import { useThrottleFn } from 'ahooks'; import { useThrottleFn } from 'ahooks';
import Conversation from './Conversation';
import RightSection from './RightSection'; import RightSection from './RightSection';
import ChatFooter from './ChatFooter'; import ChatFooter from './ChatFooter';
import classNames from 'classnames'; import classNames from 'classnames';
import { AUTH_TOKEN_KEY, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from '@/common/constants'; import { CHAT_TITLE, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from './constants';
import { import { cloneDeep } from 'lodash';
HistoryMsgItemType, import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-sdk';
MsgDataType,
getHistoryMsg,
queryContext,
setToken as setChatSdkToken,
} from 'supersonic-chat-sdk';
import { getConversationContext } from './utils';
import 'supersonic-chat-sdk/dist/index.css'; import 'supersonic-chat-sdk/dist/index.css';
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
import { TOKEN_KEY } from '@/services/request';
type Props = {
isCopilotMode?: boolean;
};
const Chat: React.FC<Props> = ({ isCopilotMode }) => {
const isMobileMode = (isMobile || isCopilotMode) as boolean;
const Chat = () => {
const [messageList, setMessageList] = useState<MessageItem[]>([]); const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [inputMsg, setInputMsg] = useState(''); const [inputMsg, setInputMsg] = useState('');
const [pageNo, setPageNo] = useState(1); const [pageNo, setPageNo] = useState(1);
@@ -29,15 +30,14 @@ const Chat = () => {
const [historyInited, setHistoryInited] = useState(false); const [historyInited, setHistoryInited] = useState(false);
const [currentConversation, setCurrentConversation] = useState< const [currentConversation, setCurrentConversation] = useState<
ConversationDetailType | undefined ConversationDetailType | undefined
>(isMobile ? { chatId: 0, chatName: '问答对话' } : undefined); >(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
const [currentEntity, setCurrentEntity] = useState<MsgDataType>(); const [currentEntity, setCurrentEntity] = useState<MsgDataType>();
const [miniProgramLoading, setMiniProgramLoading] = useState(false);
const [domains, setDomains] = useState<DomainType[]>([]);
const [currentDomain, setCurrentDomain] = useState<DomainType>();
const conversationRef = useRef<any>(); const conversationRef = useRef<any>();
const chatFooterRef = useRef<any>(); const chatFooterRef = useRef<any>();
useEffect(() => {
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
}, []);
const sendHelloRsp = () => { const sendHelloRsp = () => {
setMessageList([ setMessageList([
{ {
@@ -48,15 +48,35 @@ const Chat = () => {
]); ]);
}; };
const existInstuctionMsg = (list: HistoryMsgItemType[]) => {
return list.some((msg) => msg.queryResponse.queryMode === MessageTypeEnum.INSTRUCTION);
};
const updateScroll = (list: HistoryMsgItemType[]) => {
if (existInstuctionMsg(list)) {
setMiniProgramLoading(true);
setTimeout(() => {
setMiniProgramLoading(false);
updateMessageContainerScroll();
}, 3000);
} else {
updateMessageContainerScroll();
}
};
const updateHistoryMsg = async (page: number) => { const updateHistoryMsg = async (page: number) => {
const res = await getHistoryMsg(page, currentConversation!.chatId); const res = await getHistoryMsg(page, currentConversation!.chatId, 3);
const { hasNextPage, list } = res.data.data; const { hasNextPage, list } = res.data?.data || { hasNextPage: false, list: [] };
setMessageList([ setMessageList([
...list.map((item: HistoryMsgItemType) => ({ ...list.map((item: HistoryMsgItemType) => ({
id: item.questionId, id: item.questionId,
type: MessageTypeEnum.QUESTION, type:
item.queryResponse?.queryMode === MessageTypeEnum.INSTRUCTION
? MessageTypeEnum.INSTRUCTION
: MessageTypeEnum.QUESTION,
msg: item.queryText, msg: item.queryText,
msgData: item.queryResponse, msgData: item.queryResponse,
isHistory: true,
})), })),
...(page === 1 ? [] : messageList), ...(page === 1 ? [] : messageList),
]); ]);
@@ -67,8 +87,9 @@ const Chat = () => {
} else { } else {
setCurrentEntity(list[list.length - 1].queryResponse); setCurrentEntity(list[list.length - 1].queryResponse);
} }
updateMessageContainerScroll(); updateScroll(list);
setHistoryInited(true); setHistoryInited(true);
inputFocus();
} }
if (page > 1) { if (page > 1) {
const msgEle = document.getElementById(`${messageList[0]?.id}`); const msgEle = document.getElementById(`${messageList[0]?.id}`);
@@ -90,6 +111,21 @@ const Chat = () => {
}, },
); );
const initDomains = async () => {
try {
const res = await getDomainList();
const domainList = getLeafList(res.data);
setDomains(
[{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...domainList].slice(0, 11),
);
} catch (e) {}
};
useEffect(() => {
setChatSdkToken(localStorage.getItem(TOKEN_KEY) || '');
initDomains();
}, []);
useEffect(() => { useEffect(() => {
if (historyInited) { if (historyInited) {
const messageContainerEle = document.getElementById('messageContainer'); const messageContainerEle = document.getElementById('messageContainer');
@@ -123,7 +159,7 @@ const Chat = () => {
sendHelloRsp(); sendHelloRsp();
return; return;
} }
onSendMsg(currentConversation.initMsg, [], domainId, true); onSendMsg(currentConversation.initMsg, [], domainId);
return; return;
} }
updateHistoryMsg(1); updateHistoryMsg(1);
@@ -132,32 +168,36 @@ const Chat = () => {
const modifyConversationName = async (name: string) => { const modifyConversationName = async (name: string) => {
await updateConversationName(name, currentConversation!.chatId); await updateConversationName(name, currentConversation!.chatId);
conversationRef?.current?.updateData(); if (!isMobileMode) {
window.history.replaceState('', '', `?q=${name}&cid=${currentConversation!.chatId}`); conversationRef?.current?.updateData();
window.history.replaceState('', '', `?q=${name}&cid=${currentConversation!.chatId}`);
}
}; };
const onSendMsg = async ( const onSendMsg = async (msg?: string, list?: MessageItem[], domainId?: number) => {
msg?: string,
list?: MessageItem[],
domainId?: number,
firstMsg?: boolean,
) => {
const currentMsg = msg || inputMsg; const currentMsg = msg || inputMsg;
if (currentMsg.trim() === '') { if (currentMsg.trim() === '') {
setInputMsg(''); setInputMsg('');
return; return;
} }
let quote = ''; const msgDomain = domains.find((item) => currentMsg.includes(item.name));
if (currentEntity && !firstMsg) { const certainDomain = currentMsg[0] === '@' && msgDomain;
const { data } = await queryContext(currentMsg, currentConversation!.chatId); if (certainDomain) {
if (data.code === 200 && data.data.domainId === currentEntity.chatContext?.domainId) { setCurrentDomain(msgDomain.id === -1 ? undefined : msgDomain);
quote = getConversationContext(data.data);
}
} }
setMessageList([ const domainIdValue = domainId || msgDomain?.id || currentDomain?.id;
const msgs = [
...(list || messageList), ...(list || messageList),
{ id: uuid(), msg: currentMsg, domainId, type: MessageTypeEnum.QUESTION, quote }, {
]); id: uuid(),
msg: currentMsg,
msgValue: certainDomain ? currentMsg.replace(`@${msgDomain.name}`, '').trim() : currentMsg,
domainId: domainIdValue === -1 ? undefined : domainIdValue,
identityMsg: certainDomain ? getIdentityMsgText(msgDomain) : undefined,
type: MessageTypeEnum.QUESTION,
},
];
setMessageList(msgs);
updateMessageContainerScroll(); updateMessageContainerScroll();
setInputMsg(''); setInputMsg('');
modifyConversationName(currentMsg); modifyConversationName(currentMsg);
@@ -179,36 +219,89 @@ const Chat = () => {
}; };
const onSelectConversation = (conversation: ConversationDetailType, name?: string) => { const onSelectConversation = (conversation: ConversationDetailType, name?: string) => {
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`); if (!isMobileMode) {
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
}
setCurrentConversation({ setCurrentConversation({
...conversation, ...conversation,
initMsg: name, initMsg: name,
}); });
saveConversationToLocal(conversation); saveConversationToLocal(conversation);
setCurrentDomain(undefined);
}; };
const onMsgDataLoaded = (data: MsgDataType) => { const onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => {
if (!data) {
return;
}
if (data.queryMode === 'INSTRUCTION') {
setMessageList([
...messageList.slice(0, messageList.length - 1),
{
id: uuid(),
msg: data.response.name || messageList[messageList.length - 1]?.msg,
type: MessageTypeEnum.INSTRUCTION,
msgData: data,
},
]);
} else {
const msgs = cloneDeep(messageList);
const msg = msgs.find((item) => item.id === questionId);
if (msg) {
msg.msgData = data;
setMessageList(msgs);
}
updateMessageContainerScroll();
}
setCurrentEntity(data); setCurrentEntity(data);
};
const onCheckMore = (data: MsgDataType) => {
setMessageList([
...messageList,
{
id: uuid(),
msg: data.response.name,
type: MessageTypeEnum.INSTRUCTION,
msgData: data,
},
]);
updateMessageContainerScroll(); updateMessageContainerScroll();
}; };
const getIdentityMsgText = (domain?: DomainType) => {
return domain
? `您好,我当前身份是【${domain.name}】主题专家,我将尽力帮您解答相关问题~`
: '您好,我将尽力帮您解答所有主题相关问题~';
};
const getIdentityMsg = (domain?: DomainType) => {
return {
id: uuid(),
type: MessageTypeEnum.TEXT,
msg: getIdentityMsgText(domain),
};
};
const onSelectDomain = (domain: DomainType) => {
const domainValue = currentDomain?.id === domain.id ? undefined : domain;
setCurrentDomain(domainValue);
setCurrentEntity(undefined);
setMessageList([...messageList, getIdentityMsg(domainValue)]);
updateMessageContainerScroll();
inputFocus();
};
const chatClass = classNames(styles.chat, { const chatClass = classNames(styles.chat, {
[styles.external]: true, [styles.mobile]: isMobileMode,
[styles.mobile]: isMobile, [styles.copilot]: isCopilotMode,
}); });
return ( return (
<div className={chatClass}> <div className={chatClass}>
<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}>
{!isMobile && (
<Conversation
currentConversation={currentConversation}
onSelectConversation={onSelectConversation}
ref={conversationRef}
/>
)}
<div className={styles.chatApp}> <div className={styles.chatApp}>
{currentConversation && ( {currentConversation && (
<div className={styles.chatBody}> <div className={styles.chatBody}>
@@ -217,16 +310,22 @@ const Chat = () => {
id="messageContainer" id="messageContainer"
messageList={messageList} messageList={messageList}
chatId={currentConversation?.chatId} chatId={currentConversation?.chatId}
miniProgramLoading={miniProgramLoading}
isMobileMode={isMobileMode}
onClickMessageContainer={() => { onClickMessageContainer={() => {
inputFocus(); inputFocus();
}} }}
onMsgDataLoaded={onMsgDataLoaded} onMsgDataLoaded={onMsgDataLoaded}
onSelectSuggestion={onSendMsg} onSelectSuggestion={onSendMsg}
onCheckMore={onCheckMore}
onUpdateMessageScroll={updateMessageContainerScroll} onUpdateMessageScroll={updateMessageContainerScroll}
/> />
<ChatFooter <ChatFooter
inputMsg={inputMsg} inputMsg={inputMsg}
chatId={currentConversation?.chatId} chatId={currentConversation?.chatId}
domains={domains}
currentDomain={currentDomain}
isMobileMode={isMobileMode}
onInputMsgChange={onInputMsgChange} onInputMsgChange={onInputMsgChange}
onSendMsg={(msg: string, domainId?: number) => { onSendMsg={(msg: string, domainId?: number) => {
onSendMsg(msg, messageList, domainId); onSendMsg(msg, messageList, domainId);
@@ -234,13 +333,27 @@ const Chat = () => {
inputBlur(); inputBlur();
} }
}} }}
onAddConversation={() => {
conversationRef.current?.onAddConversation();
inputFocus();
}}
ref={chatFooterRef} ref={chatFooterRef}
/> />
</div> </div>
</div> </div>
)} )}
</div> </div>
{!isMobile && <RightSection currentEntity={currentEntity} />} {!isMobileMode && (
<RightSection
domains={domains}
currentEntity={currentEntity}
currentDomain={currentDomain}
currentConversation={currentConversation}
onSelectDomain={onSelectDomain}
onSelectConversation={onSelectConversation}
conversationRef={conversationRef}
/>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,9 +1,12 @@
import { request } from 'umi'; import { request } from 'umi';
import { DomainType } from './type';
const prefix = '/api'; const prefix = '/api';
export function saveConversation(chatName: string) { export function saveConversation(chatName: string) {
return request<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`, { method: 'POST' }); return request<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`, {
method: 'POST',
});
} }
export function updateConversationName(chatName: string, chatId: number = 0) { export function updateConversationName(chatName: string, chatId: number = 0) {
@@ -20,3 +23,17 @@ export function deleteConversation(chatId: number) {
export function getAllConversations() { export function getAllConversations() {
return request<Result<any>>(`${prefix}/chat/manage/getAll`); return request<Result<any>>(`${prefix}/chat/manage/getAll`);
} }
export function getMiniProgramList(id: string, type: string) {
return request<Result<any>>(`/openapi/bd-bi/api/polaris/sql/getInterpretList/${id}/${type}`, {
method: 'GET',
skipErrorHandler: true,
});
}
export function getDomainList() {
return request<Result<DomainType[]>>(`${prefix}/semantic/domain/getDomainList`, {
method: 'GET',
skipErrorHandler: true,
});
}

View File

@@ -1,167 +1,258 @@
@import '~antd/es/style/themes/default.less';
.chat { .chat {
height: calc(100vh - 48px) !important; height: calc(100vh - 48px) !important;
overflow-y: hidden; overflow: hidden;
background: linear-gradient(180deg, rgba(23, 74, 228, 0) 29.44%, rgba(23, 74, 228, 0.06) 100%), background: linear-gradient(180deg, rgba(23, 74, 228, 0) 29.44%, rgba(23, 74, 228, 0.06) 100%),
linear-gradient(90deg, #f3f3f7 0%, #f3f3f7 20%, #ebf0f9 60%, #f3f3f7 80%, #f3f3f7 100%); linear-gradient(90deg, #f3f3f7 0%, #f3f3f7 20%, #ebf0f9 60%, #f3f3f7 80%, #f3f3f7 100%);
&.external { .chatSection {
.chatApp { display: flex;
width: calc(100vw - 450px) !important; width: 100vw !important;
height: calc(100vh - 58px) !important; height: calc(100vh - 48px) !important;
overflow: hidden;
}
.chatApp {
display: flex;
flex-direction: column;
width: calc(100vw - 225px);
height: calc(100vh - 48px);
padding-left: 20px;
color: rgba(0, 0, 0, 0.87);
.emptyHolder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.navBar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
height: 40px;
padding: 0 10px;
background: rgb(243 243 243);
border-bottom: 1px solid rgb(228, 228, 228);
.conversationNameWrapper {
display: flex;
align-items: center;
.conversationName {
padding: 4px 12px;
color: var(--text-color-third) !important;
font-size: 14px !important;
border-radius: 4px;
cursor: pointer;
.editIcon {
margin-left: 10px;
color: var(--text-color-fourth);
font-size: 14px;
}
&:hover {
background-color: rgba(0, 0, 0, 0.03);
}
}
.divider {
width: 1px;
height: 16px;
margin-right: 4px;
margin-left: 12px;
background-color: var(--text-color-fourth);
}
}
.conversationInput {
width: 300px;
color: var(--text-color-third) !important;
font-size: 14px !important;
cursor: default !important;
}
}
.chatBody {
display: flex;
flex: 1;
height: 100%;
.chatContent {
display: flex;
flex-direction: column;
width: 100%;
.messageContainer {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow-x: hidden;
overflow-y: scroll;
.messageList {
display: flex;
flex-direction: column;
padding: 20px 20px 90px 4px;
row-gap: 10px;
.messageItem {
display: flex;
flex-direction: column;
row-gap: 10px;
:global {
.ant-table-row {
background-color: #fff;
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s, border-color 0.2s;
}
.ss-chat-table-even-row {
background-color: #fbfbfb;
}
.ant-table-wrapper .ant-table-pagination {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 16px 0;
row-gap: 8px;
}
.ant-pagination .ant-pagination-prev,
.ant-pagination .ant-pagination-next {
display: inline-block;
min-width: 32px;
height: 32px;
color: rgba(0, 0, 0, 0.88);
line-height: 32px;
text-align: center;
vertical-align: middle;
list-style: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
.ant-pagination-item-link {
display: block;
width: 100%;
height: 100%;
padding: 0;
font-size: 12px;
text-align: center;
background-color: transparent;
border: 1px solid transparent;
border-radius: 6px;
outline: none;
transition: border 0.2s;
}
}
.ant-pagination-jump-prev,
.ant-pagination-jump-next {
.ant-pagination-item-link {
display: inline-block;
min-width: 32px;
height: 32px;
color: rgba(0, 0, 0, 0.25);
line-height: 32px;
text-align: center;
vertical-align: middle;
list-style: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
}
.ant-pagination-options {
display: inline-block;
margin-left: 16px;
vertical-align: middle;
}
.ant-pagination .ant-pagination-item {
display: inline-block;
min-width: 32px;
height: 32px;
line-height: 30px;
text-align: center;
vertical-align: middle;
list-style: none;
background-color: transparent;
border: 1px solid transparent;
border-radius: 6px;
outline: 0;
cursor: pointer;
user-select: none;
margin-inline-end: 8px;
}
.ant-pagination .ant-pagination-item-active {
font-weight: 600;
background-color: #ffffff;
border-color: var(--primary-color);
}
.ant-pagination {
box-sizing: border-box;
margin: 0;
padding: 0;
color: #606266;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
font-feature-settings: 'tnum', 'tnum';
}
}
}
&.miniProgramLoading {
position: absolute;
bottom: 10000px;
width: 100%;
}
}
}
}
} }
} }
&.mobile { &.mobile {
height: 100vh !important; height: 100% !important;
.chatSection { .chatSection {
// height: 100vh !important; width: 100% !important;
height: 100% !important; height: 100% !important;
} }
.conversation { .conversation {
// height: 100vh !important;
height: 100% !important; height: 100% !important;
} }
.chatApp { .chatApp {
width: 100vw !important; width: calc(100% - 225px) !important;
// height: 100vh !important;
height: 100% !important; height: 100% !important;
} margin-top: 0 !important;
}
}
.chatSection {
display: flex;
height: calc(100vh - 48px) !important;
overflow-y: hidden;
}
.chatBody {
height: 100%;
}
.conversation {
position: relative;
width: 225px;
height: calc(100vh - 48px);
.leftSection {
width: 100%;
height: 100%;
}
}
.chatApp {
display: flex;
flex-direction: column;
width: calc(100vw - 510px);
height: calc(100vh - 58px) !important;
margin-top: 10px;
color: rgba(0, 0, 0, 0.87);
.emptyHolder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.navBar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
height: 40px;
padding: 0 10px;
background: rgb(243 243 243);
border-bottom: 1px solid rgb(228, 228, 228);
.conversationNameWrapper {
display: flex;
align-items: center;
.conversationName {
padding: 4px 12px;
color: var(--text-color-third) !important;
font-size: 14px !important;
border-radius: 4px;
cursor: pointer;
.editIcon {
margin-left: 10px;
color: var(--text-color-fourth);
font-size: 14px;
}
&:hover {
background-color: rgba(0, 0, 0, 0.03);
}
}
.divider {
width: 1px;
height: 16px;
margin-right: 4px;
margin-left: 12px;
background-color: var(--text-color-fourth);
}
}
.conversationInput {
width: 300px;
color: var(--text-color-third) !important;
font-size: 14px !important;
cursor: default !important;
}
}
.chatBody {
display: flex;
flex: 1;
.chatContent {
display: flex;
flex-direction: column;
width: 100%;
.messageContainer {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow-x: hidden;
overflow-y: scroll;
.messageList {
display: flex;
flex-direction: column;
padding: 0 20px 90px 4px;
row-gap: 20px;
.messageItem {
display: flex;
flex-direction: column;
row-gap: 20px;
}
&.reportLoading {
position: absolute;
bottom: 10000px;
width: 100%;
}
}
}
} }
} }
} }
.mobile { .mobile {
.messageList { .messageList {
padding: 0 12px 20px !important; padding: 20px 12px 20px !important;
} }
} }
@@ -235,7 +326,7 @@
} }
:global { :global {
button[ant-click-animating-without-extra-node]::after { button[ant-click-animating-without-extra-node]:after {
border: 0 none; border: 0 none;
opacity: 0; opacity: 0;
animation: none 0 ease 0 1 normal; animation: none 0 ease 0 1 normal;
@@ -358,42 +449,6 @@
} }
} }
.conversationList {
padding-top: 20px;
.conversationItem {
padding-left: 16px;
cursor: pointer;
.conversationItemContent {
display: flex;
align-items: center;
padding: 12px 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);
}
}
}
}
.addConversation { .addConversation {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -437,17 +492,6 @@
} }
} }
.collapseBtn {
margin: 0 10px;
color: var(--text-color-third);
font-size: 16px;
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
.autoCompleteDropdown { .autoCompleteDropdown {
width: 650px !important; width: 650px !important;
min-width: 650px !important; min-width: 650px !important;
@@ -462,10 +506,6 @@
background: #f5f5f5 !important; background: #f5f5f5 !important;
} }
} }
// .ant-select-item-option-active:not(.ant-select-item-option-disabled) {
// background-color: #fff;
// }
} }
} }
@@ -545,35 +585,20 @@
font-size: 12px; font-size: 12px;
} }
.operateSection { .messageLoading {
margin-top: 20px; margin-top: 30px;
padding-left: 15px; padding: 0 20px;
} }
.operateItem { :global {
display: flex; .ss-chat-recommend-options {
align-items: center; .ant-table-thead .ant-table-cell {
padding: 10px 0; padding: 8px !important;
cursor: pointer; }
.operateIcon { .ant-table-tbody .ant-table-cell {
margin-right: 10px; padding: 8px !important;
color: var(--text-color-fourth); border-bottom: 1px solid #f0f0f0;
font-size: 20px;
}
.operateLabel {
color: var(--text-color-third);
font-size: 14px;
}
&:hover {
.operateLabel {
color: var(--chat-blue);
} }
} }
} }
.messageLoading {
margin-top: 30px;
}

View File

@@ -3,19 +3,21 @@ import { MsgDataType } from 'supersonic-chat-sdk';
export enum MessageTypeEnum { export enum MessageTypeEnum {
TEXT = 'text', // 指标文本 TEXT = 'text', // 指标文本
QUESTION = 'question', QUESTION = 'question',
TAG = 'tag', // 标签
SUGGESTION = 'suggestion', // 建议
NO_PERMISSION = 'no_permission', // 无权限 NO_PERMISSION = 'no_permission', // 无权限
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情 SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
INSTRUCTION = 'INSTRUCTION', // 插件
} }
export type MessageItem = { export type MessageItem = {
id: string | number; id: string | number;
type?: MessageTypeEnum; type?: MessageTypeEnum;
msg?: string; msg?: string;
msgValue?: string;
identityMsg?: string;
domainId?: number; domainId?: number;
msgData?: MsgDataType; msgData?: MsgDataType;
quote?: string; quote?: string;
isHistory?: boolean;
}; };
export type ConversationDetailType = { export type ConversationDetailType = {
@@ -32,3 +34,10 @@ export type ConversationDetailType = {
export enum MessageModeEnum { export enum MessageModeEnum {
INTERPRET = 'interpret', INTERPRET = 'interpret',
} }
export type DomainType = {
id: number;
parentId: number;
name: string;
bizName: string;
};

View File

@@ -1,16 +0,0 @@
import { ChatContextType } from 'supersonic-chat-sdk';
import moment from 'moment';
export function getConversationContext(chatContext: ChatContextType) {
if (!chatContext) return '';
const { domainName, metrics, dateInfo } = chatContext;
// const dimensionStr =
// dimensions?.length > 0 ? dimensions.map((dimension) => dimension.name).join('、') : '';
const timeStr =
dateInfo?.text ||
`${moment(dateInfo?.endDate).diff(moment(dateInfo?.startDate), 'days') + 1}`;
return `${domainName}${
metrics?.length > 0 ? `${timeStr}${metrics.map((metric) => metric.name).join('、')}` : ''
}`;
}

View File

@@ -1,4 +1,3 @@
import { FROM_URL_KEY } from '@/common/constants';
import type { import type {
RequestOptionsInit, RequestOptionsInit,
RequestOptionsWithoutResponse, RequestOptionsWithoutResponse,

View File

@@ -343,3 +343,48 @@ export const getFormattedValueData = (value: number | string, remainZero?: boole
} }
return `${formattedValue}${unit === NumericUnit.None ? '' : unit}`; return `${formattedValue}${unit === NumericUnit.None ? '' : unit}`;
}; };
function getLeafNodes(treeNodes: any[]): any[] {
const leafNodes: any[] = [];
function traverse(node: any) {
if (!node.children || node.children.length === 0) {
leafNodes.push(node);
} else {
node.children.forEach((child: any) => traverse(child));
}
}
treeNodes.forEach((node) => traverse(node));
return leafNodes;
}
function buildTree(nodes: any[]): any[] {
const map: Record<number, any> = {};
const roots: any[] = [];
nodes.forEach((node) => {
map[node.id] = node;
node.children = [];
});
nodes.forEach((node) => {
if (node.parentId) {
const parent = map[node.parentId];
if (parent) {
parent.children.push(node);
}
} else {
roots.push(node);
}
});
return roots;
}
export function getLeafList(flatNodes: any[]): any[] {
const treeNodes = buildTree(flatNodes);
const leafNodes = getLeafNodes(treeNodes);
return leafNodes;
}