mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 11:07:06 +00:00
[feature](webapp) upgrade chat version
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,4 +13,5 @@ assembly/runtime/*
|
|||||||
**/dist/
|
**/dist/
|
||||||
*.umi/
|
*.umi/
|
||||||
/assembly/deploy
|
/assembly/deploy
|
||||||
|
/runtime
|
||||||
.flattened-pom.xml
|
.flattened-pom.xml
|
||||||
3
webapp/packages/chat-sdk/.gitignore
vendored
3
webapp/packages/chat-sdk/.gitignore
vendored
@@ -13,9 +13,6 @@
|
|||||||
|
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -191,4 +191,4 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0"
|
"node": ">=14.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
@@ -6,7 +7,10 @@ const config = {
|
|||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: 'dist/index.es.js',
|
file: 'dist/index.es.js',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
|
// plugins: [
|
||||||
|
// terser()
|
||||||
|
// ],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 || ''}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`}>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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`}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type {
|
|||||||
ChatContextType,
|
ChatContextType,
|
||||||
MsgValidTypeEnum,
|
MsgValidTypeEnum,
|
||||||
MsgDataType,
|
MsgDataType,
|
||||||
|
InstructionResonseType,
|
||||||
ColumnType,
|
ColumnType,
|
||||||
SuggestionItemType,
|
SuggestionItemType,
|
||||||
SuggestionType,
|
SuggestionType,
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,4 +67,11 @@ 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
&-metric {
|
&-metric {
|
||||||
&::after {
|
&::after {
|
||||||
background: #31c462;
|
background: var(--primary-green);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,3 +27,4 @@
|
|||||||
@import "../components/Tools/style.less";
|
@import "../components/Tools/style.less";
|
||||||
|
|
||||||
@import "../components/Suggestion/style.less";
|
@import "../components/Suggestion/style.less";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
2
webapp/packages/supersonic-fe/.gitignore
vendored
2
webapp/packages/supersonic-fe/.gitignore
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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中挂载裸露的全局变量还需要配置eslint,ts相关配置才能导致在使用中不会飘红,冗余较高,这里挂在进程环境下
|
API_BASE_URL: '/api/semantic/', // 直接在define中挂载裸露的全局变量还需要配置eslint,ts相关配置才能导致在使用中不会飘红,冗余较高,这里挂在进程环境下
|
||||||
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: [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
2
webapp/packages/supersonic-fe/config/envConfig.ts
Normal file
2
webapp/packages/supersonic-fe/config/envConfig.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const ENV_CONFIG = {};
|
||||||
|
export default ENV_CONFIG;
|
||||||
@@ -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",
|
||||||
@@ -144,4 +144,4 @@
|
|||||||
"@types/react": "17.0.0"
|
"@types/react": "17.0.0"
|
||||||
},
|
},
|
||||||
"__npminstall_done": false
|
"__npminstall_done": false
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -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);
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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%;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 = '请输入您的问题'
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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('、')}` : ''
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FROM_URL_KEY } from '@/common/constants';
|
|
||||||
import type {
|
import type {
|
||||||
RequestOptionsInit,
|
RequestOptionsInit,
|
||||||
RequestOptionsWithoutResponse,
|
RequestOptionsWithoutResponse,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user