mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 19:51:00 +00:00
add chat plugin and split query to parse and execute (#25)
* [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) gitignore add supersonic-webapp * [feature](webapp) gitignore add supersonic-webapp * [feature](webapp) add chat plugin and split query to parse and execute * [feature](webapp) add chat plugin and split query to parse and execute * [feature](webapp) add chat plugin and split query to parse and execute --------- Co-authored-by: williamhliu <williamhliu@tencent.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "supersonic-chat-sdk",
|
"name": "supersonic-chat-sdk",
|
||||||
"version": "0.0.0",
|
"version": "0.3.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": {
|
||||||
|
"@uiw/react-watermark": "^0.0.5",
|
||||||
"antd": "^5.5.2",
|
"antd": "^5.5.2",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
@@ -191,4 +192,4 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0"
|
"node": ">=14.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
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 = {
|
||||||
@@ -8,9 +7,6 @@ const config = {
|
|||||||
{
|
{
|
||||||
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 = {
|
||||||
|
|||||||
@@ -42,9 +42,18 @@ export const THEME_COLOR_LIST = [
|
|||||||
|
|
||||||
export const PARSE_ERROR_TIP = '小Q不太懂您说什么呐,回去一定补充知识';
|
export const PARSE_ERROR_TIP = '小Q不太懂您说什么呐,回去一定补充知识';
|
||||||
|
|
||||||
|
export const SEARCH_EXCEPTION_TIP = '查询出错啦,智能小Q还不够聪明,请您换个表达再试试';
|
||||||
|
|
||||||
export const MSG_VALID_TIP = {
|
export const MSG_VALID_TIP = {
|
||||||
[MsgValidTypeEnum.SEARCH_EXCEPTION]: '数据查询异常',
|
[MsgValidTypeEnum.SEARCH_EXCEPTION]: '数据查询异常',
|
||||||
[MsgValidTypeEnum.INVALID]: '小Q不太懂您说什么呐,回去一定补充知识',
|
[MsgValidTypeEnum.INVALID]: '小Q不太懂您说什么呐,回去一定补充知识',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREFIX_CLS = 'ss-chat';
|
export const PREFIX_CLS = 'ss-chat';
|
||||||
|
|
||||||
|
export const AGG_TYPE_MAP = {
|
||||||
|
SUM: '总计',
|
||||||
|
AVG: '平均值',
|
||||||
|
MAX: '最大值',
|
||||||
|
MIN: '最小值',
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ export type FieldType = {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
domain: number;
|
||||||
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ export type EntityInfoType = {
|
|||||||
|
|
||||||
export type DateInfoType = {
|
export type DateInfoType = {
|
||||||
dateList: any[];
|
dateList: any[];
|
||||||
dateMode: number;
|
dateMode: string;
|
||||||
period: string;
|
period: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
@@ -56,8 +58,11 @@ export type ChatContextType = {
|
|||||||
dateInfo: DateInfoType;
|
dateInfo: DateInfoType;
|
||||||
dimensions: FieldType[];
|
dimensions: FieldType[];
|
||||||
metrics: FieldType[];
|
metrics: FieldType[];
|
||||||
entity: number;
|
entity: { alias: string[] };
|
||||||
|
elementMatches: any[];
|
||||||
|
queryMode: string;
|
||||||
dimensionFilters: FilterItemType[];
|
dimensionFilters: FilterItemType[];
|
||||||
|
properties: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum MsgValidTypeEnum {
|
export enum MsgValidTypeEnum {
|
||||||
@@ -67,16 +72,11 @@ export enum MsgValidTypeEnum {
|
|||||||
INVALID = 3,
|
INVALID = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InstructionResonseType = {
|
export type PluginResonseType = {
|
||||||
description: string;
|
description: string;
|
||||||
instructionConfig: {
|
webPage: { url: string, paramOptions: any, params: any, valueParams: any };
|
||||||
showElements: { elementId: string, params: any }[];
|
pluginId: number;
|
||||||
showType: string;
|
pluginType: string;
|
||||||
relaShowElements: { elementId: string, params: any }[];
|
|
||||||
relaShowType: string;
|
|
||||||
};
|
|
||||||
instructionId: number;
|
|
||||||
instructionType: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +103,23 @@ export type MsgDataType = {
|
|||||||
queryId: number;
|
queryId: number;
|
||||||
queryMode: string;
|
queryMode: string;
|
||||||
queryState: string;
|
queryState: string;
|
||||||
response: InstructionResonseType;
|
response: PluginResonseType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ParseStateEnum {
|
||||||
|
COMPLETED = 'COMPLETED',
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
FAILED = 'FAILED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParseDataType = {
|
||||||
|
chatId: number;
|
||||||
|
queryText: string;
|
||||||
|
state: ParseStateEnum;
|
||||||
|
selectedParses: ChatContextType[];
|
||||||
|
candidateParses: ChatContextType[];
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryDataType = {
|
export type QueryDataType = {
|
||||||
queryColumns: ColumnType[];
|
queryColumns: ColumnType[];
|
||||||
queryResults: any[];
|
queryResults: any[];
|
||||||
@@ -120,7 +134,7 @@ export type ColumnType = {
|
|||||||
dataFormatType: string;
|
dataFormatType: string;
|
||||||
dataFormat: {
|
dataFormat: {
|
||||||
decimalPlaces: number;
|
decimalPlaces: number;
|
||||||
needmultiply100: boolean;
|
needMultiply100: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { PREFIX_CLS } from '../../common/constants';
|
||||||
|
import { MsgDataType } from '../../common/type';
|
||||||
|
import ChatMsg from '../ChatMsg';
|
||||||
|
import Tools from '../Tools';
|
||||||
|
import Text from './Text';
|
||||||
|
import Typing from './Typing';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
question: string;
|
||||||
|
executeLoading: boolean;
|
||||||
|
chartIndex: number;
|
||||||
|
executeTip?: string;
|
||||||
|
data?: MsgDataType;
|
||||||
|
isMobileMode?: boolean;
|
||||||
|
triggerResize?: boolean;
|
||||||
|
isLastMessage?: boolean;
|
||||||
|
onSwitchEntity: (entityId: string) => void;
|
||||||
|
onChangeChart: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExecuteItem: React.FC<Props> = ({
|
||||||
|
question,
|
||||||
|
executeLoading,
|
||||||
|
chartIndex,
|
||||||
|
executeTip,
|
||||||
|
data,
|
||||||
|
isMobileMode,
|
||||||
|
triggerResize,
|
||||||
|
isLastMessage,
|
||||||
|
onSwitchEntity,
|
||||||
|
onChangeChart,
|
||||||
|
}) => {
|
||||||
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
|
||||||
|
if (executeLoading) {
|
||||||
|
return <Typing />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (executeTip) {
|
||||||
|
return <Text data={executeTip} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.queryMode === 'WEB_PAGE') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMetricCard =
|
||||||
|
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||||
|
data.queryResults?.length === 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${prefixCls}-msg-content`}>
|
||||||
|
<ChatMsg
|
||||||
|
question={question}
|
||||||
|
data={data}
|
||||||
|
chartIndex={chartIndex}
|
||||||
|
isMobileMode={isMobileMode}
|
||||||
|
triggerResize={triggerResize}
|
||||||
|
/>
|
||||||
|
{!isMetricCard && (
|
||||||
|
<Tools
|
||||||
|
data={data}
|
||||||
|
isLastMessage={isLastMessage}
|
||||||
|
isMobileMode={isMobileMode}
|
||||||
|
onSwitchEntity={onSwitchEntity}
|
||||||
|
onChangeChart={onChangeChart}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExecuteItem;
|
||||||
205
webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx
Normal file
205
webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||||
|
import { ChatContextType } from '../../common/type';
|
||||||
|
import Text from './Text';
|
||||||
|
import Typing from './Typing';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
parseLoading: boolean;
|
||||||
|
parseInfoOptions: ChatContextType[];
|
||||||
|
parseTip: string;
|
||||||
|
currentParseInfo?: ChatContextType;
|
||||||
|
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_OPTION_VALUES_COUNT = 2;
|
||||||
|
|
||||||
|
const ParseTip: React.FC<Props> = ({
|
||||||
|
parseLoading,
|
||||||
|
parseInfoOptions,
|
||||||
|
parseTip,
|
||||||
|
currentParseInfo,
|
||||||
|
onSelectParseInfo,
|
||||||
|
}) => {
|
||||||
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
|
||||||
|
if (parseLoading) {
|
||||||
|
return <Typing />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseTip) {
|
||||||
|
return <Text data={parseTip} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseInfoOptions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||||
|
const {
|
||||||
|
domainName,
|
||||||
|
dateInfo,
|
||||||
|
dimensionFilters,
|
||||||
|
dimensions,
|
||||||
|
metrics,
|
||||||
|
aggType,
|
||||||
|
queryMode,
|
||||||
|
properties,
|
||||||
|
entity,
|
||||||
|
elementMatches,
|
||||||
|
} = parseInfo || {};
|
||||||
|
const { startDate, endDate } = dateInfo || {};
|
||||||
|
const dimensionItems = dimensions?.filter(item => item.type === 'DIMENSION');
|
||||||
|
const metric = metrics?.[0];
|
||||||
|
|
||||||
|
const tipContentClass = classNames(`${prefixCls}-tip-content`, {
|
||||||
|
[`${prefixCls}-tip-content-option`]: isOptions,
|
||||||
|
[`${prefixCls}-tip-content-option-active`]:
|
||||||
|
isOptions &&
|
||||||
|
currentParseInfo &&
|
||||||
|
JSON.stringify(currentParseInfo) === JSON.stringify(parseInfo),
|
||||||
|
[`${prefixCls}-tip-content-option-disabled`]:
|
||||||
|
isOptions &&
|
||||||
|
currentParseInfo !== undefined &&
|
||||||
|
JSON.stringify(currentParseInfo) !== JSON.stringify(parseInfo),
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemValueClass = classNames({
|
||||||
|
[`${prefixCls}-tip-item-value`]: !isOptions,
|
||||||
|
[`${prefixCls}-tip-item-option`]: isOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||||
|
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||||
|
|
||||||
|
const pluginName = properties?.CONTEXT?.plugin?.name;
|
||||||
|
|
||||||
|
const modeName = pluginName
|
||||||
|
? '调插件'
|
||||||
|
: queryMode.includes('METRIC')
|
||||||
|
? '算指标'
|
||||||
|
: queryMode === 'ENTITY_DETAIL'
|
||||||
|
? '查明细'
|
||||||
|
: queryMode === 'ENTITY_LIST_FILTER'
|
||||||
|
? '做圈选'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const fields =
|
||||||
|
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tipContentClass}
|
||||||
|
onClick={() => {
|
||||||
|
if (isOptions && currentParseInfo === undefined) {
|
||||||
|
onSelectParseInfo(parseInfo);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index !== undefined && <div>{index + 1}.</div>}
|
||||||
|
{!pluginName && isOptions && <div className={`${prefixCls}-mode-name`}>{modeName}:</div>}
|
||||||
|
{!!pluginName ? (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
将由问答插件
|
||||||
|
<span className={itemValueClass}>{pluginName}</span>来解答
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL' ? (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}:</div>
|
||||||
|
<div className={itemValueClass}>{entityName}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>主题域:</div>
|
||||||
|
<div className={itemValueClass}>{domainName}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{modeName === '算指标' && metric && (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
||||||
|
<div className={itemValueClass}>{metric.name}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isOptions && (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>时间:</div>
|
||||||
|
<div className={itemValueClass}>
|
||||||
|
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{['METRIC_GROUPBY', 'METRIC_ORDERBY', 'ENTITY_DETAIL'].includes(queryMode) &&
|
||||||
|
fields &&
|
||||||
|
fields.length > 0 && (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>
|
||||||
|
{queryMode === 'ENTITY_DETAIL' ? '查询字段' : '下钻维度'}:
|
||||||
|
</div>
|
||||||
|
<div className={itemValueClass}>
|
||||||
|
{fields
|
||||||
|
.slice(0, MAX_OPTION_VALUES_COUNT)
|
||||||
|
.map(field => field.name)
|
||||||
|
.join('、')}
|
||||||
|
{fields.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{['METRIC_FILTER', 'METRIC_ENTITY', 'ENTITY_DETAIL', 'ENTITY_LIST_FILTER'].includes(
|
||||||
|
queryMode
|
||||||
|
) &&
|
||||||
|
dimensionFilters &&
|
||||||
|
dimensionFilters?.length > 0 && (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||||
|
{dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT).map((filter, index) => (
|
||||||
|
<div className={itemValueClass}>
|
||||||
|
<span>{filter.name}:</span>
|
||||||
|
<span>
|
||||||
|
{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}
|
||||||
|
</span>
|
||||||
|
{index !== dimensionFilters.length - 1 && <span>、</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{dimensionFilters.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
|
||||||
|
<div className={`${prefixCls}-tip-item`}>
|
||||||
|
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
||||||
|
<div className={itemValueClass}>{AGG_TYPE_MAP[aggType]}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let tipNode: ReactNode;
|
||||||
|
|
||||||
|
if (parseInfoOptions.length > 1) {
|
||||||
|
tipNode = (
|
||||||
|
<div className={`${prefixCls}-multi-options`}>
|
||||||
|
<div>您的问题解析为以下几项,请您点击确认</div>
|
||||||
|
<div className={`${prefixCls}-options`}>
|
||||||
|
{parseInfoOptions.map((item, index) => getTipNode(item, true, index))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const pluginName = parseInfoOptions[0]?.properties?.CONTEXT?.plugin?.name;
|
||||||
|
tipNode = (
|
||||||
|
<div className={`${prefixCls}-tip`}>
|
||||||
|
<div>{!!pluginName ? '您的问题' : '您的问题解析为:'}</div>
|
||||||
|
{getTipNode(parseInfoOptions[0])}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text data={tipNode} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ParseTip;
|
||||||
@@ -1,71 +1,112 @@
|
|||||||
import { MsgDataType } from '../../common/type';
|
import { ChatContextType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Typing from './Typing';
|
import { chatExecute, chatParse, switchEntity } from '../../service';
|
||||||
import ChatMsg from '../ChatMsg';
|
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||||
import { chatQuery } from '../../service';
|
|
||||||
import { PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
|
||||||
import Text from './Text';
|
|
||||||
import Tools from '../Tools';
|
|
||||||
import IconFont from '../IconFont';
|
import IconFont from '../IconFont';
|
||||||
|
import ParseTip from './ParseTip';
|
||||||
|
import ExecuteItem from './ExecuteItem';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
msg: string;
|
msg: string;
|
||||||
followQuestions?: string[];
|
|
||||||
conversationId?: number;
|
conversationId?: number;
|
||||||
domainId?: number;
|
domainId?: number;
|
||||||
|
filter?: any[];
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
msgData?: MsgDataType;
|
msgData?: MsgDataType;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
onMsgDataLoaded?: (data: MsgDataType) => void;
|
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
||||||
|
onUpdateMessageScroll?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatItem: React.FC<Props> = ({
|
const ChatItem: React.FC<Props> = ({
|
||||||
msg,
|
msg,
|
||||||
followQuestions,
|
|
||||||
conversationId,
|
conversationId,
|
||||||
domainId,
|
domainId,
|
||||||
|
filter,
|
||||||
isLastMessage,
|
isLastMessage,
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
triggerResize,
|
triggerResize,
|
||||||
msgData,
|
msgData,
|
||||||
onMsgDataLoaded,
|
onMsgDataLoaded,
|
||||||
|
onUpdateMessageScroll,
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<MsgDataType>();
|
const [data, setData] = useState<MsgDataType>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [parseLoading, setParseLoading] = useState(false);
|
||||||
const [tip, setTip] = useState('');
|
const [parseInfo, setParseInfo] = useState<ChatContextType>();
|
||||||
|
const [parseInfoOptions, setParseInfoOptions] = useState<ChatContextType[]>([]);
|
||||||
|
const [parseTip, setParseTip] = useState('');
|
||||||
|
const [executeLoading, setExecuteLoading] = useState(false);
|
||||||
|
const [executeTip, setExecuteTip] = useState('');
|
||||||
|
const [executeMode, setExecuteMode] = useState(false);
|
||||||
|
const [entitySwitching, setEntitySwitching] = useState(false);
|
||||||
|
|
||||||
|
const [chartIndex, setChartIndex] = useState(0);
|
||||||
|
|
||||||
|
const prefixCls = `${PREFIX_CLS}-item`;
|
||||||
|
|
||||||
const updateData = (res: Result<MsgDataType>) => {
|
const updateData = (res: Result<MsgDataType>) => {
|
||||||
if (res.code === 401 || res.code === 412) {
|
if (res.code === 401 || res.code === 412) {
|
||||||
setTip(res.msg);
|
setExecuteTip(res.msg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
setTip(PARSE_ERROR_TIP);
|
setExecuteTip(SEARCH_EXCEPTION_TIP);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
|
const { queryColumns, queryResults, queryState, queryMode, response } = res.data || {};
|
||||||
if (queryState !== 'SUCCESS') {
|
if (queryState !== 'SUCCESS') {
|
||||||
setTip(PARSE_ERROR_TIP);
|
setExecuteTip(response && typeof response === 'string' ? response : SEARCH_EXCEPTION_TIP);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
|
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'WEB_PAGE') {
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
setTip('');
|
setExecuteTip('');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
setTip(PARSE_ERROR_TIP);
|
setExecuteTip(SEARCH_EXCEPTION_TIP);
|
||||||
return false;
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExecute = async (parseInfoValue: ChatContextType, isSwitch?: boolean) => {
|
||||||
|
setExecuteMode(true);
|
||||||
|
setExecuteLoading(true);
|
||||||
|
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||||
|
setExecuteLoading(false);
|
||||||
|
const valid = updateData(data);
|
||||||
|
if (onMsgDataLoaded && !isSwitch) {
|
||||||
|
onMsgDataLoaded({ ...data.data, chatContext: parseInfoValue }, valid);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSendMsg = async () => {
|
const onSendMsg = async () => {
|
||||||
setLoading(true);
|
setParseLoading(true);
|
||||||
const semanticRes = await chatQuery(msg, conversationId, domainId);
|
const { data: parseData } = await chatParse(msg, conversationId, domainId, filter);
|
||||||
updateData(semanticRes.data);
|
setParseLoading(false);
|
||||||
if (onMsgDataLoaded) {
|
const { code, data } = parseData || {};
|
||||||
onMsgDataLoaded(semanticRes.data.data);
|
const { state, selectedParses } = data || {};
|
||||||
|
if (
|
||||||
|
code !== 200 ||
|
||||||
|
state === ParseStateEnum.FAILED ||
|
||||||
|
selectedParses == null ||
|
||||||
|
selectedParses.length === 0 ||
|
||||||
|
(selectedParses.length === 1 &&
|
||||||
|
!selectedParses[0]?.domainName &&
|
||||||
|
!selectedParses[0]?.properties?.CONTEXT?.plugin?.name &&
|
||||||
|
selectedParses[0]?.queryMode !== 'WEB_PAGE')
|
||||||
|
) {
|
||||||
|
setParseTip(PARSE_ERROR_TIP);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onUpdateMessageScroll) {
|
||||||
|
onUpdateMessageScroll();
|
||||||
|
}
|
||||||
|
setParseInfoOptions(selectedParses || []);
|
||||||
|
if (selectedParses.length === 1) {
|
||||||
|
const parseInfoValue = selectedParses[0];
|
||||||
|
setParseInfo(parseInfoValue);
|
||||||
|
onExecute(parseInfoValue);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,55 +114,66 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msgData) {
|
if (msgData) {
|
||||||
|
setParseInfoOptions([msgData.chatContext]);
|
||||||
|
setExecuteMode(true);
|
||||||
updateData({ code: 200, data: msgData, msg: 'success' });
|
updateData({ code: 200, data: msgData, msg: 'success' });
|
||||||
} else if (msg) {
|
} else if (msg) {
|
||||||
onSendMsg();
|
onSendMsg();
|
||||||
}
|
}
|
||||||
}, [msg, msgData]);
|
}, [msg, msgData]);
|
||||||
|
|
||||||
const prefixCls = `${PREFIX_CLS}-item`;
|
const onSwitchEntity = async (entityId: string) => {
|
||||||
|
setEntitySwitching(true);
|
||||||
|
const res = await switchEntity(entityId, data?.chatContext?.domainId, conversationId || 0);
|
||||||
|
setEntitySwitching(false);
|
||||||
|
setData(res.data.data);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
const onChangeChart = () => {
|
||||||
return (
|
setChartIndex(chartIndex + 1);
|
||||||
<div className={prefixCls}>
|
};
|
||||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
|
||||||
<Typing />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tip) {
|
const onSelectParseInfo = async (parseInfoValue: ChatContextType) => {
|
||||||
return (
|
setParseInfo(parseInfoValue);
|
||||||
<div className={prefixCls}>
|
onExecute(parseInfoValue, parseInfo !== undefined);
|
||||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
if (onUpdateMessageScroll) {
|
||||||
<Text data={tip} />
|
onUpdateMessageScroll();
|
||||||
</div>
|
}
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || data.queryMode === 'INSTRUCTION') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMetricCard =
|
|
||||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
|
||||||
data.queryResults?.length === 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
<div className={`${prefixCls}-section`}>
|
||||||
<div className={`${prefixCls}-content`}>
|
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||||
<ChatMsg
|
<div className={`${prefixCls}-content`}>
|
||||||
question={msg}
|
<ParseTip
|
||||||
followQuestions={followQuestions}
|
parseLoading={parseLoading}
|
||||||
data={data}
|
parseInfoOptions={parseInfoOptions}
|
||||||
isMobileMode={isMobileMode}
|
parseTip={parseTip}
|
||||||
triggerResize={triggerResize}
|
currentParseInfo={parseInfo}
|
||||||
/>
|
onSelectParseInfo={onSelectParseInfo}
|
||||||
{!isMetricCard && (
|
/>
|
||||||
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{executeMode && data?.queryMode !== 'WEB_PAGE' && (
|
||||||
|
<div className={`${prefixCls}-section`}>
|
||||||
|
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||||
|
<div className={`${prefixCls}-content`}>
|
||||||
|
<ExecuteItem
|
||||||
|
question={msg}
|
||||||
|
executeLoading={executeLoading}
|
||||||
|
executeTip={executeTip}
|
||||||
|
chartIndex={chartIndex}
|
||||||
|
data={data}
|
||||||
|
isMobileMode={isMobileMode}
|
||||||
|
isLastMessage={isLastMessage}
|
||||||
|
triggerResize={triggerResize}
|
||||||
|
onSwitchEntity={onSwitchEntity}
|
||||||
|
onChangeChart={onChangeChart}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,101 @@
|
|||||||
|
|
||||||
.@{chat-item-prefix-cls} {
|
.@{chat-item-prefix-cls} {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&-section {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content-text {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-msg-content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-multi-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 12px;
|
||||||
|
padding: 4px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
row-gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 6px;
|
||||||
|
column-gap: 12px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-content-option {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--border-color-base);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--chat-blue);
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-content-option-disabled {
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
border-color: var(--border-color-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-content-option-active {
|
||||||
|
border-color: var(--chat-blue);
|
||||||
|
color: var(--chat-blue);
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-mode-name {
|
||||||
|
margin-right: -10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-item-value {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tip-item-option {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
&-avatar {
|
&-avatar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -41,7 +136,7 @@
|
|||||||
|
|
||||||
&-typing-bubble {
|
&-typing-bubble {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 16px !important;
|
// padding: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-text-bubble {
|
&-text-bubble {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const BarChart: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { queryColumns, queryResults, entityInfo, chatContext, queryMode } = data;
|
const { queryColumns, queryResults, entityInfo, chatContext, queryMode } = data;
|
||||||
|
|
||||||
const { dateInfo } = chatContext || {};
|
const { dateInfo, dimensionFilters } = chatContext || {};
|
||||||
|
|
||||||
const categoryColumnName =
|
const categoryColumnName =
|
||||||
queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || '';
|
queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || '';
|
||||||
@@ -51,13 +51,6 @@ const BarChart: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
const xData = data.map(item => item[categoryColumnName]);
|
const xData = data.map(item => item[categoryColumnName]);
|
||||||
instanceObj.setOption({
|
instanceObj.setOption({
|
||||||
// legend: {
|
|
||||||
// left: 0,
|
|
||||||
// top: 0,
|
|
||||||
// icon: 'rect',
|
|
||||||
// itemWidth: 15,
|
|
||||||
// itemHeight: 5,
|
|
||||||
// },
|
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
axisTick: {
|
axisTick: {
|
||||||
@@ -166,21 +159,43 @@ const BarChart: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFilterSection = dimensionFilters?.length > 0;
|
||||||
|
|
||||||
|
const prefixCls = `${PREFIX_CLS}-bar`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${PREFIX_CLS}-bar-metric-name`}>{metricColumn?.name}</div>
|
<div className={`${prefixCls}-top-bar`}>
|
||||||
<FilterSection chatContext={chatContext} />
|
<div className={`${prefixCls}-indicator-name`}>{metricColumn?.name}</div>
|
||||||
|
{(hasFilterSection || drillDownDimension) && (
|
||||||
|
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||||
|
(
|
||||||
|
<div className={`${prefixCls}-filter-section`}>
|
||||||
|
<FilterSection chatContext={chatContext} entityInfo={entityInfo} />
|
||||||
|
{drillDownDimension && (
|
||||||
|
<div className={`${prefixCls}-filter-item`}>
|
||||||
|
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||||
|
<div className={`${prefixCls}-filter-item-value`}>{drillDownDimension.name}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{dateInfo && (
|
{dateInfo && (
|
||||||
<div className={`${PREFIX_CLS}-bar-date-range`}>
|
<div className={`${prefixCls}-date-range`}>
|
||||||
{dateInfo.startDate === dateInfo.endDate
|
{dateInfo.startDate === dateInfo.endDate
|
||||||
? dateInfo.startDate
|
? dateInfo.startDate
|
||||||
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className={`${PREFIX_CLS}-bar-chart`} ref={chartRef} />
|
<div className={`${prefixCls}-chart`} ref={chartRef} />
|
||||||
</Spin>
|
</Spin>
|
||||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
{(queryMode === 'METRIC_DOMAIN' ||
|
||||||
|
queryMode === 'METRIC_FILTER' ||
|
||||||
|
queryMode === 'METRIC_GROUPBY') && (
|
||||||
<DrillDownDimensions
|
<DrillDownDimensions
|
||||||
domainId={chatContext.domainId}
|
domainId={chatContext.domainId}
|
||||||
drillDownDimension={drillDownDimension}
|
drillDownDimension={drillDownDimension}
|
||||||
|
|||||||
@@ -9,11 +9,49 @@
|
|||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metric-name {
|
&-top-bar {
|
||||||
font-size: 15px;
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 8px;
|
||||||
|
row-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-section-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
column-gap: 12px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item-label {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item-value {
|
||||||
|
color: var(--text-color);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-indicator-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&-date-range {
|
&-date-range {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { PREFIX_CLS } from '../../../common/constants';
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
import { ChatContextType } from '../../../common/type';
|
import { ChatContextType, EntityInfoType } from '../../../common/type';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
chatContext?: ChatContextType;
|
chatContext?: ChatContextType;
|
||||||
|
entityInfo?: EntityInfoType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FilterSection: React.FC<Props> = ({ chatContext }) => {
|
const FilterSection: React.FC<Props> = ({ chatContext, entityInfo }) => {
|
||||||
const prefixCls = `${PREFIX_CLS}-filter-section`;
|
const prefixCls = `${PREFIX_CLS}-filter-section`;
|
||||||
|
|
||||||
|
const entityInfoList =
|
||||||
|
entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || [];
|
||||||
|
|
||||||
const { dimensionFilters } = chatContext || {};
|
const { dimensionFilters } = chatContext || {};
|
||||||
|
|
||||||
const hasFilterSection = dimensionFilters && dimensionFilters.length > 0;
|
const hasFilterSection = dimensionFilters && dimensionFilters.length > 0;
|
||||||
@@ -16,7 +20,7 @@ const FilterSection: React.FC<Props> = ({ chatContext }) => {
|
|||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-field-label`}>筛选条件:</div>
|
<div className={`${prefixCls}-field-label`}>筛选条件:</div>
|
||||||
<div className={`${prefixCls}-filter-values`}>
|
<div className={`${prefixCls}-filter-values`}>
|
||||||
{dimensionFilters.map(filterItem => {
|
{(entityInfoList.length > 0 ? entityInfoList : dimensionFilters).map(filterItem => {
|
||||||
const filterValue =
|
const filterValue =
|
||||||
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,21 +5,25 @@
|
|||||||
.@{filter-section-prefix-cls} {
|
.@{filter-section-prefix-cls} {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 12px;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
||||||
|
&-field-label {
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
}
|
||||||
|
|
||||||
&-filter-values {
|
&-filter-values {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 6px;
|
flex-wrap: wrap;
|
||||||
|
column-gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-filter-item {
|
&-filter-item {
|
||||||
padding: 2px 12px;
|
|
||||||
color: var(--text-color-third);
|
color: var(--text-color-third);
|
||||||
background-color: #edf2f2;
|
|
||||||
border-radius: 13px;
|
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -12,67 +12,28 @@ type Props = {
|
|||||||
entityInfo?: EntityInfoType;
|
entityInfo?: EntityInfoType;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
|
queryMode?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Message: React.FC<Props> = ({
|
const Message: React.FC<Props> = ({
|
||||||
position,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
title,
|
|
||||||
followQuestions,
|
|
||||||
children,
|
children,
|
||||||
bubbleClassName,
|
bubbleClassName,
|
||||||
chatContext,
|
|
||||||
entityInfo,
|
entityInfo,
|
||||||
isMobileMode,
|
queryMode,
|
||||||
|
chatContext,
|
||||||
}) => {
|
}) => {
|
||||||
const { dimensionFilters, domainName } = chatContext || {};
|
|
||||||
|
|
||||||
const prefixCls = `${PREFIX_CLS}-message`;
|
const prefixCls = `${PREFIX_CLS}-message`;
|
||||||
|
|
||||||
|
const { domainName, dateInfo, dimensionFilters } = chatContext || {};
|
||||||
|
const { startDate, endDate } = dateInfo || {};
|
||||||
|
|
||||||
const entityInfoList =
|
const entityInfoList =
|
||||||
entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || [];
|
entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || [];
|
||||||
|
|
||||||
const hasFilterSection =
|
|
||||||
dimensionFilters && dimensionFilters.length > 0 && entityInfoList.length === 0;
|
|
||||||
|
|
||||||
const filterSection = hasFilterSection && (
|
|
||||||
<div className={`${prefixCls}-filter-section`}>
|
|
||||||
<div className={`${prefixCls}-field-name`}>筛选条件:</div>
|
|
||||||
<div className={`${prefixCls}-filter-values`}>
|
|
||||||
{dimensionFilters.map(filterItem => {
|
|
||||||
const filterValue =
|
|
||||||
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${prefixCls}-filter-item`}
|
|
||||||
key={filterItem.name}
|
|
||||||
title={filterValue.join('、')}
|
|
||||||
>
|
|
||||||
{filterItem.name}:{filterValue.join('、')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const leftTitle = title
|
|
||||||
? followQuestions && followQuestions.length > 0
|
|
||||||
? `多轮对话:${[title, ...followQuestions].join(' ← ')}`
|
|
||||||
: `单轮对话:${title}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-title-bar`}>
|
|
||||||
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
|
|
||||||
{position === 'left' && leftTitle && (
|
|
||||||
<div className={`${prefixCls}-top-bar`} title={leftTitle}>
|
|
||||||
({leftTitle})
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`${prefixCls}-content`}>
|
<div className={`${prefixCls}-content`}>
|
||||||
<div className={`${prefixCls}-body`}>
|
<div className={`${prefixCls}-body`}>
|
||||||
<div
|
<div
|
||||||
@@ -82,10 +43,9 @@ const Message: React.FC<Props> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{entityInfoList.length > 0 && (
|
{(queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL') &&
|
||||||
<div className={`${prefixCls}-info-bar`}>
|
entityInfoList.length > 0 && (
|
||||||
{/* {filterSection} */}
|
<div className={`${prefixCls}-info-bar`}>
|
||||||
{entityInfoList.length > 0 && (
|
|
||||||
<div className={`${prefixCls}-main-entity-info`}>
|
<div className={`${prefixCls}-main-entity-info`}>
|
||||||
{entityInfoList.slice(0, 4).map(dimension => {
|
{entityInfoList.slice(0, 4).map(dimension => {
|
||||||
return (
|
return (
|
||||||
@@ -100,7 +60,36 @@ const Message: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{queryMode === 'ENTITY_LIST_FILTER' && (
|
||||||
|
<div className={`${prefixCls}-info-bar`}>
|
||||||
|
<div className={`${prefixCls}-main-entity-info`}>
|
||||||
|
<div className={`${prefixCls}-info-item`}>
|
||||||
|
<div className={`${prefixCls}-info-name`}>主题域:</div>
|
||||||
|
<div className={`${prefixCls}-info-value`}>{domainName}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${prefixCls}-info-item`}>
|
||||||
|
<div className={`${prefixCls}-info-name`}>时间:</div>
|
||||||
|
<div className={`${prefixCls}-info-value`}>
|
||||||
|
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{dimensionFilters && dimensionFilters?.length > 0 && (
|
||||||
|
<div className={`${prefixCls}-info-item`}>
|
||||||
|
<div className={`${prefixCls}-info-name`}>筛选条件:</div>
|
||||||
|
{dimensionFilters.map((filter, index) => (
|
||||||
|
<div className={`${prefixCls}-info-value`}>
|
||||||
|
<span>{filter.name}:</span>
|
||||||
|
<span>
|
||||||
|
{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}
|
||||||
|
</span>
|
||||||
|
{index !== dimensionFilters.length - 1 && <span>、</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`${prefixCls}-children`}>{children}</div>
|
<div className={`${prefixCls}-children`}>{children}</div>
|
||||||
|
|||||||
@@ -83,7 +83,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
row-gap: 12px;
|
row-gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
|
margin-bottom: 12px;
|
||||||
column-gap: 20px;
|
column-gap: 20px;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
background: rgba(133, 156, 241, 0.1);
|
background: rgba(133, 156, 241, 0.1);
|
||||||
@@ -96,7 +97,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 13px;
|
|
||||||
column-gap: 20px;
|
column-gap: 20px;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
}
|
}
|
||||||
@@ -107,11 +107,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-info-name {
|
&-info-name {
|
||||||
color: var(--text-color-third);
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-info-value {
|
&-info-value {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PREFIX_CLS } from '../../../common/constants';
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
import { formatByThousandSeperator } from '../../../utils/utils';
|
import { formatMetric } from '../../../utils/utils';
|
||||||
import ApplyAuth from '../ApplyAuth';
|
import ApplyAuth from '../ApplyAuth';
|
||||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||||
import PeriodCompareItem from './PeriodCompareItem';
|
import PeriodCompareItem from './PeriodCompareItem';
|
||||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import FilterSection from '../FilterSection';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
@@ -25,8 +26,8 @@ const MetricCard: React.FC<Props> = ({
|
|||||||
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||||
|
|
||||||
const { metricInfos } = aggregateInfo || {};
|
const { metricInfos } = aggregateInfo || {};
|
||||||
const { dateInfo } = chatContext || {};
|
const { dateInfo, dimensionFilters } = chatContext || {};
|
||||||
const { startDate, endDate } = dateInfo || {};
|
const { startDate } = dateInfo || {};
|
||||||
|
|
||||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||||
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
||||||
@@ -37,19 +38,36 @@ const MetricCard: React.FC<Props> = ({
|
|||||||
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasFilterSection = dimensionFilters?.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
<div className={`${prefixCls}-top-bar`}>
|
||||||
|
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||||
|
{(hasFilterSection || drillDownDimension) && (
|
||||||
|
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||||
|
(
|
||||||
|
<div className={`${prefixCls}-filter-section`}>
|
||||||
|
<FilterSection chatContext={chatContext} entityInfo={entityInfo} />
|
||||||
|
{drillDownDimension && (
|
||||||
|
<div className={`${prefixCls}-filter-item`}>
|
||||||
|
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||||
|
<div className={`${prefixCls}-filter-item-value`}>{drillDownDimension.name}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className={indicatorClass}>
|
<div className={indicatorClass}>
|
||||||
<div className={`${prefixCls}-date-range`}>
|
<div className={`${prefixCls}-date-range`}>{startDate}</div>
|
||||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
|
||||||
</div>
|
|
||||||
{indicatorColumn && !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`}>
|
||||||
{formatByThousandSeperator(queryResults?.[0]?.[indicatorColumnName])}
|
{formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{metricInfos?.length > 0 && (
|
{metricInfos?.length > 0 && (
|
||||||
|
|||||||
@@ -7,6 +7,41 @@
|
|||||||
height: 130px;
|
height: 130px;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
|
|
||||||
|
&-top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-section-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
column-gap: 12px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item-label {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item-value {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
&-indicator-name {
|
&-indicator-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -74,7 +109,7 @@
|
|||||||
|
|
||||||
&-drill-down-dimensions {
|
&-drill-down-dimensions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -38px;
|
bottom: -44px;
|
||||||
left: 0;
|
left: -16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PREFIX_CLS } from '../../../common/constants';
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
import { formatByThousandSeperator } from '../../../utils/utils';
|
import { formatMetric } from '../../../utils/utils';
|
||||||
import { AggregateInfoType } from '../../../common/type';
|
import { AggregateInfoType } from '../../../common/type';
|
||||||
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
|||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-indicator`}>
|
<div className={`${prefixCls}-indicator`}>
|
||||||
<div className={`${prefixCls}-date`}>{date}</div>
|
<div className={`${prefixCls}-date`}>{date}</div>
|
||||||
<div className={`${prefixCls}-indicator-value`}>{formatByThousandSeperator(value)}</div>
|
<div className={`${prefixCls}-indicator-value`}>{formatMetric(value)}</div>
|
||||||
{metricInfos?.length > 0 && (
|
{metricInfos?.length > 0 && (
|
||||||
<div className={`${prefixCls}-period-compare`}>
|
<div className={`${prefixCls}-period-compare`}>
|
||||||
{Object.keys(statistics).map((key: any) => (
|
{Object.keys(statistics).map((key: any) => (
|
||||||
|
|||||||
@@ -46,8 +46,17 @@ const MetricTrendChart: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const valueColumnName = metricField.nameEn;
|
const valueColumnName = metricField.nameEn;
|
||||||
const groupDataValue = groupByColumn(resultList, categoryColumnName);
|
const dataSource = resultList.map((item: any) => {
|
||||||
const [startDate, endDate] = getMinMaxDate(resultList, dateColumnName);
|
return {
|
||||||
|
...item,
|
||||||
|
[dateColumnName]: Array.isArray(item[dateColumnName])
|
||||||
|
? moment(item[dateColumnName].join('')).format('MM-DD')
|
||||||
|
: item[dateColumnName],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDataValue = groupByColumn(dataSource, categoryColumnName);
|
||||||
|
const [startDate, endDate] = getMinMaxDate(dataSource, dateColumnName);
|
||||||
const groupData = Object.keys(groupDataValue).reduce((result: any, key) => {
|
const groupData = Object.keys(groupDataValue).reduce((result: any, key) => {
|
||||||
result[key] =
|
result[key] =
|
||||||
startDate &&
|
startDate &&
|
||||||
@@ -61,7 +70,7 @@ const MetricTrendChart: React.FC<Props> = ({
|
|||||||
endDate,
|
endDate,
|
||||||
dateColumnName.includes('month') ? 'months' : 'days'
|
dateColumnName.includes('month') ? 'months' : 'days'
|
||||||
)
|
)
|
||||||
: groupDataValue[key].reverse();
|
: groupDataValue[key];
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
@@ -167,7 +176,7 @@ const MetricTrendChart: React.FC<Props> = ({
|
|||||||
data: data.map((item: any) => {
|
data: data.map((item: any) => {
|
||||||
const value = item[valueColumnName];
|
const value = item[valueColumnName];
|
||||||
return metricField.dataFormatType === 'percent' &&
|
return metricField.dataFormatType === 'percent' &&
|
||||||
metricField.dataFormat?.needmultiply100
|
metricField.dataFormat?.needMultiply100
|
||||||
? value * 100
|
? value * 100
|
||||||
: value;
|
: value;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,29 +10,33 @@ import Table from '../Table';
|
|||||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||||
import MetricInfo from './MetricInfo';
|
import MetricInfo from './MetricInfo';
|
||||||
import FilterSection from '../FilterSection';
|
import FilterSection from '../FilterSection';
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
|
chartIndex: number;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
onApplyAuth?: (domain: string) => void;
|
onApplyAuth?: (domain: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApplyAuth }) => {
|
||||||
const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data;
|
const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data;
|
||||||
|
|
||||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
const { dateMode, unit } = chatContext?.dateInfo || {};
|
||||||
const initialDateOption = dateOptions.find(
|
|
||||||
(option: any) => option.value === chatContext?.dateInfo?.unit
|
|
||||||
)?.value;
|
|
||||||
|
|
||||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
||||||
|
const initialDateOption = dateOptions.find((option: any) => {
|
||||||
|
return dateMode === 'RECENT' && option.value === unit;
|
||||||
|
})?.value;
|
||||||
|
|
||||||
|
const [columns, setColumns] = useState<ColumnType[]>(queryColumns || []);
|
||||||
const currentMetricField = columns.find((column: any) => column.showType === 'NUMBER');
|
const currentMetricField = columns.find((column: any) => column.showType === 'NUMBER');
|
||||||
|
|
||||||
const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
|
const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
|
||||||
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||||
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
|
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
|
||||||
|
const [dimensions, setDimensions] = useState<FieldType[]>(chatContext?.dimensions);
|
||||||
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||||
|
const [dateModeValue, setDateModeValue] = useState(dateMode);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const dateField: any = columns.find(
|
const dateField: any = columns.find(
|
||||||
@@ -46,6 +50,18 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
setDataSource(queryResults);
|
setDataSource(queryResults);
|
||||||
}, [queryResults]);
|
}, [queryResults]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryMode === 'METRIC_GROUPBY') {
|
||||||
|
const dimensionValue = chatContext?.dimensions?.find(
|
||||||
|
dimension => dimension.type === 'DIMENSION'
|
||||||
|
);
|
||||||
|
setDrillDownDimension(dimensionValue);
|
||||||
|
setDimensions(
|
||||||
|
chatContext?.dimensions?.filter(dimension => dimension.id !== dimensionValue?.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onLoadData = async (value: any) => {
|
const onLoadData = async (value: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { data } = await queryData({
|
const { data } = await queryData({
|
||||||
@@ -61,19 +77,13 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
|
|
||||||
const selectDateOption = (dateOption: number) => {
|
const selectDateOption = (dateOption: number) => {
|
||||||
setCurrentDateOption(dateOption);
|
setCurrentDateOption(dateOption);
|
||||||
const endDate = moment().subtract(1, 'days').format('YYYY-MM-DD');
|
setDateModeValue('RECENT');
|
||||||
const startDate = moment(endDate)
|
|
||||||
.subtract(dateOption - 1, 'days')
|
|
||||||
.format('YYYY-MM-DD');
|
|
||||||
onLoadData({
|
onLoadData({
|
||||||
metrics: [activeMetricField],
|
metrics: [activeMetricField],
|
||||||
dimensions: drillDownDimension
|
dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined,
|
||||||
? [...(chatContext.dimensions || []), drillDownDimension]
|
|
||||||
: undefined,
|
|
||||||
dateInfo: {
|
dateInfo: {
|
||||||
...chatContext?.dateInfo,
|
...chatContext?.dateInfo,
|
||||||
startDate,
|
dateMode: 'RECENT',
|
||||||
endDate,
|
|
||||||
unit: dateOption,
|
unit: dateOption,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -82,10 +92,12 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
const onSwitchMetric = (metricField: FieldType) => {
|
const onSwitchMetric = (metricField: FieldType) => {
|
||||||
setActiveMetricField(metricField);
|
setActiveMetricField(metricField);
|
||||||
onLoadData({
|
onLoadData({
|
||||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
dateInfo: {
|
||||||
dimensions: drillDownDimension
|
...chatContext.dateInfo,
|
||||||
? [...(chatContext.dimensions || []), drillDownDimension]
|
dateMode: dateModeValue,
|
||||||
: undefined,
|
unit: currentDateOption || chatContext.dateInfo.unit,
|
||||||
|
},
|
||||||
|
dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined,
|
||||||
metrics: [metricField],
|
metrics: [metricField],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -93,10 +105,13 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
||||||
setDrillDownDimension(dimension);
|
setDrillDownDimension(dimension);
|
||||||
onLoadData({
|
onLoadData({
|
||||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
dateInfo: {
|
||||||
|
...chatContext.dateInfo,
|
||||||
|
dateMode: dateModeValue,
|
||||||
|
unit: currentDateOption || chatContext.dateInfo.unit,
|
||||||
|
},
|
||||||
metrics: [activeMetricField],
|
metrics: [activeMetricField],
|
||||||
dimensions:
|
dimensions: dimension === undefined ? undefined : [...(dimensions || []), dimension],
|
||||||
dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension],
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,36 +121,58 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
const prefixCls = `${CLS_PREFIX}-metric-trend`;
|
||||||
|
|
||||||
|
const { dimensionFilters } = chatContext || {};
|
||||||
|
|
||||||
|
const hasFilterSection = dimensionFilters?.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
<div className={`${prefixCls}-charts`}>
|
<div className={`${prefixCls}-charts`}>
|
||||||
{chatContext.metrics.length > 0 && (
|
<div className={`${prefixCls}-top-bar`}>
|
||||||
<div className={`${prefixCls}-metric-fields`}>
|
{chatContext.metrics.length > 0 && (
|
||||||
{chatContext.metrics.map((metricField: FieldType) => {
|
<div className={`${prefixCls}-metric-fields`}>
|
||||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
{chatContext.metrics.slice(0, 5).map((metricField: FieldType) => {
|
||||||
[`${prefixCls}-metric-field-active`]:
|
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
||||||
activeMetricField?.bizName === metricField.bizName &&
|
[`${prefixCls}-metric-field-active`]:
|
||||||
chatContext.metrics.length > 1,
|
activeMetricField?.bizName === metricField.bizName &&
|
||||||
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
chatContext.metrics.length > 1,
|
||||||
});
|
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
||||||
return (
|
});
|
||||||
<div
|
return (
|
||||||
className={metricFieldClass}
|
<div
|
||||||
key={metricField.bizName}
|
className={metricFieldClass}
|
||||||
onClick={() => {
|
key={metricField.bizName}
|
||||||
if (chatContext.metrics.length > 1) {
|
onClick={() => {
|
||||||
onSwitchMetric(metricField);
|
if (chatContext.metrics.length > 1) {
|
||||||
}
|
onSwitchMetric(metricField);
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
{metricField.name}
|
>
|
||||||
</div>
|
{metricField.name}
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{(hasFilterSection || drillDownDimension) && (
|
||||||
|
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||||
|
(
|
||||||
|
<div className={`${prefixCls}-filter-section`}>
|
||||||
|
<FilterSection chatContext={chatContext} />
|
||||||
|
{drillDownDimension && (
|
||||||
|
<div className={`${prefixCls}-filter-item`}>
|
||||||
|
<div className={`${prefixCls}-filter-item-label`}>下钻维度:</div>
|
||||||
|
<div className={`${prefixCls}-filter-item-value`}>
|
||||||
|
{drillDownDimension.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{aggregateInfo?.metricInfos?.length > 0 && <MetricInfo aggregateInfo={aggregateInfo} />}
|
{aggregateInfo?.metricInfos?.length > 0 && <MetricInfo aggregateInfo={aggregateInfo} />}
|
||||||
<FilterSection chatContext={chatContext} />
|
|
||||||
<div className={`${prefixCls}-date-options`}>
|
<div className={`${prefixCls}-date-options`}>
|
||||||
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
||||||
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
|
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
|
||||||
@@ -164,7 +201,7 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{dataSource?.length === 1 ? (
|
{dataSource?.length === 1 || chartIndex % 2 === 1 ? (
|
||||||
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
|
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
|
||||||
) : (
|
) : (
|
||||||
<MetricTrendChart
|
<MetricTrendChart
|
||||||
@@ -178,7 +215,9 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
{(queryMode === 'METRIC_DOMAIN' ||
|
||||||
|
queryMode === 'METRIC_FILTER' ||
|
||||||
|
queryMode === 'METRIC_GROUPBY') && (
|
||||||
<DrillDownDimensions
|
<DrillDownDimensions
|
||||||
domainId={chatContext.domainId}
|
domainId={chatContext.domainId}
|
||||||
drillDownDimension={drillDownDimension}
|
drillDownDimension={drillDownDimension}
|
||||||
|
|||||||
@@ -13,6 +13,41 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
|
|
||||||
|
&-top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-section-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
column-gap: 12px;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item-label {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filter-item-value {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
&-indicator {
|
&-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -171,4 +206,3 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,11 @@
|
|||||||
|
|
||||||
&-holder {
|
&-holder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 280px;
|
||||||
// background-image: url(~./images/line_chart_holder.png);
|
|
||||||
// background-repeat: no-repeat;
|
|
||||||
// background-size: 100% 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-bar-chart-holder {
|
&-bar-chart-holder {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
// background-image: url(~./images/bar_chart_holder.png);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-no-permission {
|
&-no-permission {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Tag } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../../../common/type';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
infoType?: SemanticTypeEnum;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SemanticTypeTag: React.FC<Props> = ({ infoType = SemanticTypeEnum.METRIC }) => {
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
color={
|
|
||||||
infoType === SemanticTypeEnum.DIMENSION || infoType === SemanticTypeEnum.DOMAIN
|
|
||||||
? 'blue'
|
|
||||||
: infoType === SemanticTypeEnum.VALUE
|
|
||||||
? 'geekblue'
|
|
||||||
: 'orange'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{SEMANTIC_TYPE_MAP[infoType]}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SemanticTypeTag;
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { Popover, message, Row, Col, Button, Spin } from 'antd';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { SemanticTypeEnum } from '../../../common/type';
|
|
||||||
import { queryMetricInfo } from '../../../service';
|
|
||||||
import SemanticTypeTag from './SemanticTypeTag';
|
|
||||||
import { isMobile } from '../../../utils/utils';
|
|
||||||
import { CLS_PREFIX } from '../../../common/constants';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
classId?: number;
|
|
||||||
infoType?: SemanticTypeEnum;
|
|
||||||
uniqueId: string | number;
|
|
||||||
onDetailBtnClick?: (data: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SemanticInfoPopover: React.FC<Props> = ({
|
|
||||||
classId,
|
|
||||||
infoType,
|
|
||||||
uniqueId,
|
|
||||||
children,
|
|
||||||
onDetailBtnClick,
|
|
||||||
}) => {
|
|
||||||
const [semanticInfo, setSemanticInfo] = useState<any>(undefined);
|
|
||||||
const [popoverVisible, setPopoverVisible] = useState<boolean>(false);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-semantic-info-popover`;
|
|
||||||
|
|
||||||
const text = (
|
|
||||||
<Row>
|
|
||||||
<Col flex="1">
|
|
||||||
<SemanticTypeTag infoType={infoType} />
|
|
||||||
</Col>
|
|
||||||
{onDetailBtnClick && (
|
|
||||||
<Col flex="0 1 40px">
|
|
||||||
{semanticInfo && (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
onDetailBtnClick(semanticInfo);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
详情
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = loading ? (
|
|
||||||
<div className={`${prefixCls}-spin-box`}>
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<span>{semanticInfo?.description || '暂无数据'}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMetricInfo = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
const { data: resData } = await queryMetricInfo({
|
|
||||||
classId,
|
|
||||||
uniqueId,
|
|
||||||
});
|
|
||||||
const { code, data, msg } = resData;
|
|
||||||
setLoading(false);
|
|
||||||
if (code === '0') {
|
|
||||||
setSemanticInfo({
|
|
||||||
...data,
|
|
||||||
semanticInfoType: SemanticTypeEnum.METRIC,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.error(msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (popoverVisible && !semanticInfo) {
|
|
||||||
getMetricInfo();
|
|
||||||
}
|
|
||||||
}, [popoverVisible]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
placement="top"
|
|
||||||
title={text}
|
|
||||||
content={content}
|
|
||||||
trigger="hover"
|
|
||||||
open={classId && !isMobile ? undefined : false}
|
|
||||||
onOpenChange={visible => {
|
|
||||||
setPopoverVisible(visible);
|
|
||||||
}}
|
|
||||||
overlayClassName={prefixCls}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SemanticInfoPopover;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
@import '../../../styles/index.less';
|
|
||||||
|
|
||||||
@semantic-info-popover-cls: ~'@{supersonic-chat-prefix}-semantic-info-popover';
|
|
||||||
|
|
||||||
.semantic-info-popover-cls {
|
|
||||||
max-width: 300px;
|
|
||||||
&-spin-box {
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
.ant-popover-title{
|
|
||||||
padding: 5px 8px 4px;
|
|
||||||
}
|
|
||||||
.ant-popover-inner-content {
|
|
||||||
min-height: 60px;
|
|
||||||
min-width: 185px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,15 @@ import { Table as AntTable } from 'antd';
|
|||||||
import { MsgDataType } from '../../../common/type';
|
import { MsgDataType } from '../../../common/type';
|
||||||
import { CLS_PREFIX } from '../../../common/constants';
|
import { CLS_PREFIX } from '../../../common/constants';
|
||||||
import ApplyAuth from '../ApplyAuth';
|
import ApplyAuth from '../ApplyAuth';
|
||||||
|
import { SizeType } from 'antd/es/config-provider/SizeContext';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
|
size?: SizeType;
|
||||||
onApplyAuth?: (domain: string) => void;
|
onApplyAuth?: (domain: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => {
|
||||||
const { entityInfo, queryColumns, queryResults } = data;
|
const { entityInfo, queryColumns, queryResults } = data;
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-table`;
|
const prefixCls = `${CLS_PREFIX}-table`;
|
||||||
@@ -19,7 +21,7 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
|||||||
return {
|
return {
|
||||||
dataIndex: nameEn,
|
dataIndex: nameEn,
|
||||||
key: nameEn,
|
key: nameEn,
|
||||||
title: name,
|
title: name || nameEn,
|
||||||
render: (value: string | number) => {
|
render: (value: string | number) => {
|
||||||
if (!authorized) {
|
if (!authorized) {
|
||||||
return (
|
return (
|
||||||
@@ -30,7 +32,7 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={`${prefixCls}-formatted-value`}>
|
<div className={`${prefixCls}-formatted-value`}>
|
||||||
{`${formatByDecimalPlaces(
|
{`${formatByDecimalPlaces(
|
||||||
dataFormat?.needmultiply100 ? +value * 100 : value,
|
dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||||
dataFormat?.decimalPlaces || 2
|
dataFormat?.decimalPlaces || 2
|
||||||
)}%`}
|
)}%`}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +73,7 @@ const Table: React.FC<Props> = ({ data, onApplyAuth }) => {
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
rowClassName={getRowClassName}
|
rowClassName={getRowClassName}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,19 +10,13 @@ import { queryData } from '../../service';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
question: string;
|
question: string;
|
||||||
followQuestions?: string[];
|
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
|
chartIndex: number;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatMsg: React.FC<Props> = ({
|
const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, triggerResize }) => {
|
||||||
question,
|
|
||||||
followQuestions,
|
|
||||||
data,
|
|
||||||
isMobileMode,
|
|
||||||
triggerResize,
|
|
||||||
}) => {
|
|
||||||
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
||||||
|
|
||||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||||
@@ -41,7 +35,10 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
||||||
|
|
||||||
const isMetricCard =
|
const isMetricCard =
|
||||||
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && singleData;
|
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') &&
|
||||||
|
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
|
||||||
|
|
||||||
|
const isText = columns.length === 1 && columns[0].showType === 'CATEGORY' && singleData;
|
||||||
|
|
||||||
const onLoadData = async (value: any) => {
|
const onLoadData = async (value: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -65,6 +62,13 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getMsgContent = () => {
|
const getMsgContent = () => {
|
||||||
|
if (isText) {
|
||||||
|
return (
|
||||||
|
<div style={{ lineHeight: '24px', width: 'fit-content' }}>
|
||||||
|
{dataSource[0][columns[0].nameEn]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isMetricCard) {
|
if (isMetricCard) {
|
||||||
return (
|
return (
|
||||||
<MetricCard
|
<MetricCard
|
||||||
@@ -88,6 +92,7 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<MetricTrend
|
<MetricTrend
|
||||||
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||||
|
chartIndex={chartIndex}
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -105,7 +110,9 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
let width = '100%';
|
let width = '100%';
|
||||||
if (isMetricCard) {
|
if (isText) {
|
||||||
|
width = 'fit-content';
|
||||||
|
} else if (isMetricCard) {
|
||||||
width = '370px';
|
width = '370px';
|
||||||
} else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
} else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||||
if (columns.length === 1) {
|
if (columns.length === 1) {
|
||||||
@@ -121,9 +128,9 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
chatContext={chatContext}
|
chatContext={chatContext}
|
||||||
entityInfo={entityInfo}
|
entityInfo={entityInfo}
|
||||||
title={question}
|
title={question}
|
||||||
followQuestions={followQuestions}
|
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
width={width}
|
width={width}
|
||||||
|
queryMode={queryMode}
|
||||||
>
|
>
|
||||||
{getMsgContent()}
|
{getMsgContent()}
|
||||||
</Message>
|
</Message>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ type Props = {
|
|||||||
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DIMENSION_COUNT = 5;
|
|
||||||
const MAX_DIMENSION_COUNT = 20;
|
const MAX_DIMENSION_COUNT = 20;
|
||||||
|
|
||||||
const DrillDownDimensions: React.FC<Props> = ({
|
const DrillDownDimensions: React.FC<Props> = ({
|
||||||
@@ -26,6 +25,8 @@ const DrillDownDimensions: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [dimensions, setDimensions] = useState<DrillDownDimensionType[]>([]);
|
const [dimensions, setDimensions] = useState<DrillDownDimensionType[]>([]);
|
||||||
|
|
||||||
|
const DEFAULT_DIMENSION_COUNT = isMetricCard ? 3 : 5;
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`;
|
const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`;
|
||||||
|
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { getFormattedValue, isMobile } from '../../utils/utils';
|
||||||
|
import { Table } from 'antd';
|
||||||
|
import Avatar from 'antd/lib/avatar/avatar';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { queryEntities } from '../../service';
|
||||||
|
import { CLS_PREFIX } from '../../common/constants';
|
||||||
|
import IconFont from '../IconFont';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entityId: string | number;
|
||||||
|
domainId: number;
|
||||||
|
domainName: string;
|
||||||
|
isMobileMode?: boolean;
|
||||||
|
onSelect: (option: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecommendOptions: React.FC<Props> = ({
|
||||||
|
entityId,
|
||||||
|
domainId,
|
||||||
|
domainName,
|
||||||
|
isMobileMode,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const prefixCls = `${CLS_PREFIX}-recommend-options`;
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await queryEntities(entityId, domainId);
|
||||||
|
setLoading(false);
|
||||||
|
setData(res.data.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (entityId) {
|
||||||
|
initData();
|
||||||
|
}
|
||||||
|
}, [entityId]);
|
||||||
|
|
||||||
|
const getSectionOptions = () => {
|
||||||
|
const basicColumn = {
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
title: '基本信息',
|
||||||
|
render: (name: string, record: any) => {
|
||||||
|
return (
|
||||||
|
<div className={`${prefixCls}-item-name-column`}>
|
||||||
|
<Avatar
|
||||||
|
shape="square"
|
||||||
|
icon={<IconFont type={domainName === '艺人库' ? 'icon-geshou' : 'icon-zhuanji'} />}
|
||||||
|
src={record.url}
|
||||||
|
/>
|
||||||
|
<div className={`${prefixCls}-entity-name`}>
|
||||||
|
{name}
|
||||||
|
{record.ver && record.ver !== '完整版' && record.ver !== '-' && `(${record.ver})`}
|
||||||
|
{record.singerName && ` - ${record.singerName}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const playCntColumnIdex = domainName.includes('歌曲')
|
||||||
|
? 'tme3platAvgLogYyPlayCnt'
|
||||||
|
: 'tme3platJsPlayCnt';
|
||||||
|
|
||||||
|
const columns = isMobile
|
||||||
|
? [basicColumn]
|
||||||
|
: [
|
||||||
|
basicColumn,
|
||||||
|
domainName.includes('艺人')
|
||||||
|
? {
|
||||||
|
dataIndex: 'onlineSongCnt',
|
||||||
|
key: 'onlineSongCnt',
|
||||||
|
title: '在架歌曲数',
|
||||||
|
align: 'center',
|
||||||
|
render: (onlineSongCnt: string) => {
|
||||||
|
return onlineSongCnt ? getFormattedValue(+onlineSongCnt) : '-';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
dataIndex: 'publishTime',
|
||||||
|
key: 'publishTime',
|
||||||
|
title: '发布时间',
|
||||||
|
align: 'center',
|
||||||
|
render: (publishTime: string) => {
|
||||||
|
return publishTime ? moment(publishTime).format('YYYY-MM-DD') : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: playCntColumnIdex,
|
||||||
|
key: playCntColumnIdex,
|
||||||
|
align: 'center',
|
||||||
|
title: domainName.includes('歌曲') ? '近7天日均运营播放量' : '昨日结算播放量',
|
||||||
|
render: (value: string) => {
|
||||||
|
return value ? getFormattedValue(+value) : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns as any}
|
||||||
|
dataSource={data}
|
||||||
|
showHeader={!isMobile}
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
loading={loading}
|
||||||
|
className={`${prefixCls}-table`}
|
||||||
|
rowClassName={`${prefixCls}-table-row`}
|
||||||
|
onRow={record => {
|
||||||
|
return {
|
||||||
|
onClick: () => {
|
||||||
|
onSelect(record.id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const recommendOptionsClass = classNames(prefixCls, {
|
||||||
|
[`${prefixCls}-mobile-mode`]: isMobileMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={recommendOptionsClass}>{getSectionOptions()}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecommendOptions;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@import '../../styles/index.less';
|
||||||
|
|
||||||
|
@recommend-options-prefix-cls: ~'@{supersonic-chat-prefix}-recommend-options';
|
||||||
|
|
||||||
|
.@{recommend-options-prefix-cls} {
|
||||||
|
padding: 8px 0 12px;
|
||||||
|
|
||||||
|
&-item-name-column {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-entity-name {
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-table-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { isMobile } from '../../utils/utils';
|
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { EntityInfoType } from '../../common/type';
|
|
||||||
import Message from '../ChatMsg/Message';
|
|
||||||
import { CLS_PREFIX } from '../../common/constants';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
currentMsgAggregator?: string;
|
|
||||||
columns: any[];
|
|
||||||
mainEntity: EntityInfoType;
|
|
||||||
suggestions: any;
|
|
||||||
onSelect?: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PAGE_SIZE = isMobile ? 3 : 5;
|
|
||||||
|
|
||||||
const Suggestion: React.FC<Props> = ({
|
|
||||||
currentMsgAggregator,
|
|
||||||
columns,
|
|
||||||
mainEntity,
|
|
||||||
suggestions,
|
|
||||||
onSelect,
|
|
||||||
}) => {
|
|
||||||
const [dimensions, setDimensions] = useState<string[]>([]);
|
|
||||||
const [metrics, setMetrics] = useState<string[]>([]);
|
|
||||||
const [dimensionIndex, setDimensionIndex] = useState(0);
|
|
||||||
const [metricIndex, setMetricIndex] = useState(0);
|
|
||||||
|
|
||||||
const fields = columns
|
|
||||||
.filter(column => currentMsgAggregator !== 'tag' || column.showType !== 'NUMBER')
|
|
||||||
.concat(isMobile ? [] : mainEntity?.dimensions || [])
|
|
||||||
.map(item => item.name);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDimensions(
|
|
||||||
suggestions.dimensions
|
|
||||||
.filter((dimension: any) => !fields.some(field => field === dimension.name))
|
|
||||||
.map((item: any) => item.name)
|
|
||||||
);
|
|
||||||
setMetrics(
|
|
||||||
suggestions.metrics
|
|
||||||
.filter((metric: any) => !fields.some(field => field === metric.name))
|
|
||||||
.map((item: any) => item.name)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reloadDimensionCmds = () => {
|
|
||||||
const dimensionPageCount = Math.ceil(dimensions.length / PAGE_SIZE);
|
|
||||||
setDimensionIndex((dimensionIndex + 1) % dimensionPageCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadMetricCmds = () => {
|
|
||||||
const metricPageCount = Math.ceil(metrics.length / PAGE_SIZE);
|
|
||||||
setMetricIndex((metricIndex + 1) % metricPageCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dimensionList = dimensions.slice(
|
|
||||||
dimensionIndex * PAGE_SIZE,
|
|
||||||
(dimensionIndex + 1) * PAGE_SIZE
|
|
||||||
);
|
|
||||||
|
|
||||||
const metricList = metrics.slice(metricIndex * PAGE_SIZE, (metricIndex + 1) * PAGE_SIZE);
|
|
||||||
|
|
||||||
if (!dimensionList.length && !metricList.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-suggestion`;
|
|
||||||
|
|
||||||
const suggestionClass = classNames(prefixCls, {
|
|
||||||
[`${prefixCls}-mobile`]: isMobile,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sectionItemClass = classNames({
|
|
||||||
[`${prefixCls}-section-item-selectable`]: onSelect !== undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={suggestionClass}>
|
|
||||||
<Message position="left" width="fit-content">
|
|
||||||
<div className={`${prefixCls}-tip`}>问答支持多轮对话,您可以继续输入:</div>
|
|
||||||
{metricList.length > 0 && (
|
|
||||||
<div className={`${prefixCls}-content-section`}>
|
|
||||||
<div className={`${prefixCls}-title`}>指标:</div>
|
|
||||||
<div className={`${prefixCls}-section-items`}>
|
|
||||||
{metricList.map((metric, index) => {
|
|
||||||
let metricNode = (
|
|
||||||
<div
|
|
||||||
className={sectionItemClass}
|
|
||||||
onClick={() => {
|
|
||||||
if (onSelect) {
|
|
||||||
onSelect(metric);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{metric}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{metricNode}
|
|
||||||
{index < metricList.length - 1 && '、'}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{metrics.length > PAGE_SIZE && (
|
|
||||||
<div
|
|
||||||
className={`${prefixCls}-reload`}
|
|
||||||
onClick={() => {
|
|
||||||
reloadMetricCmds();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReloadOutlined className={`${prefixCls}-reload-icon`} />
|
|
||||||
{!isMobile && <div className={`${prefixCls}-reload-label`}>换一批</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dimensionList.length > 0 && (
|
|
||||||
<div className={`${prefixCls}-content-section`}>
|
|
||||||
<div className={`${prefixCls}-title`}>维度:</div>
|
|
||||||
<div className={`${prefixCls}-section-items`}>
|
|
||||||
{dimensionList.map((dimension, index) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={sectionItemClass}
|
|
||||||
onClick={() => {
|
|
||||||
if (onSelect) {
|
|
||||||
onSelect(dimension);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dimension}
|
|
||||||
</div>
|
|
||||||
{index < dimensionList.length - 1 && '、'}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{dimensions.length > PAGE_SIZE && (
|
|
||||||
<div
|
|
||||||
className={`${prefixCls}-reload`}
|
|
||||||
onClick={() => {
|
|
||||||
reloadDimensionCmds();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReloadOutlined className={`${prefixCls}-reload-icon`} />
|
|
||||||
{!isMobile && <div className={`${prefixCls}-reload-label`}>换一批</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Message>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Suggestion;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
@import '../../styles/index.less';
|
|
||||||
|
|
||||||
@suggestion-prefix-cls: ~'@{supersonic-chat-prefix}-suggestion';
|
|
||||||
|
|
||||||
.@{suggestion-prefix-cls} {
|
|
||||||
margin-top: 30px;
|
|
||||||
|
|
||||||
.@{suggestion-prefix-cls}-mobile {
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-tip {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
row-gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-section-items {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-section-item-selectable {
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-reload {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 14px;
|
|
||||||
margin-left: 20px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
font-size: 12px;
|
|
||||||
column-gap: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-reload-icon {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Modal, Input, message } from 'antd';
|
||||||
|
import { CLS_PREFIX } from '../../common/constants';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
visible: boolean;
|
||||||
|
feedbackValue: string;
|
||||||
|
onSubmit: (feedback: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeedbackModal: React.FC<Props> = ({ visible, feedbackValue, onSubmit, onClose }) => {
|
||||||
|
const [feedback, setFeedback] = useState(feedbackValue);
|
||||||
|
const prefixCls = `${CLS_PREFIX}-tools`;
|
||||||
|
|
||||||
|
const onOk = () => {
|
||||||
|
if (feedback.trim() === '') {
|
||||||
|
message.warning('请输入点评内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmit(feedback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFeedbackChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setFeedback(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
title="点评一下~"
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onClose}
|
||||||
|
okText="提交"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<div className={`${prefixCls}-feedback-item`}>
|
||||||
|
<div className={`${prefixCls}-feedback-item-title`}>评价</div>
|
||||||
|
<TextArea
|
||||||
|
placeholder="请输入评价"
|
||||||
|
rows={3}
|
||||||
|
value={feedback}
|
||||||
|
onChange={onFeedbackChange}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedbackModal;
|
||||||
@@ -1,20 +1,49 @@
|
|||||||
import { isMobile } from '../../utils/utils';
|
import { isMobile } from '../../utils/utils';
|
||||||
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
||||||
import { Button, message } from 'antd';
|
import { Button, Popover, message } from 'antd';
|
||||||
import { CLS_PREFIX } from '../../common/constants';
|
import { CLS_PREFIX } from '../../common/constants';
|
||||||
import { MsgDataType } from '../../common/type';
|
import { MsgDataType } from '../../common/type';
|
||||||
|
import RecommendOptions from '../RecommendOptions';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { updateQAFeedback } from '../../service';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
|
scoreValue?: number;
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
|
onSwitchEntity: (entityId: string) => void;
|
||||||
|
onChangeChart: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tools: React.FC<Props> = ({ data, isLastMessage, isMobileMode }) => {
|
const Tools: React.FC<Props> = ({
|
||||||
|
data,
|
||||||
|
scoreValue,
|
||||||
|
isLastMessage,
|
||||||
|
isMobileMode,
|
||||||
|
onSwitchEntity,
|
||||||
|
onChangeChart,
|
||||||
|
}) => {
|
||||||
|
const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false);
|
||||||
|
const { queryColumns, queryResults, queryId, chatContext, queryMode } = data || {};
|
||||||
|
const [score, setScore] = useState(scoreValue || 0);
|
||||||
|
|
||||||
const prefixCls = `${CLS_PREFIX}-tools`;
|
const prefixCls = `${CLS_PREFIX}-tools`;
|
||||||
|
|
||||||
|
const noDashboard =
|
||||||
|
(queryColumns?.length === 1 &&
|
||||||
|
queryColumns[0].showType === 'CATEGORY' &&
|
||||||
|
queryResults?.length === 1) ||
|
||||||
|
(!queryMode.includes('METRIC') && !queryMode.includes('ENTITY'));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'chatContext?.properties?.CONTEXT?.plugin?.name',
|
||||||
|
chatContext?.properties?.CONTEXT?.plugin?.name
|
||||||
|
);
|
||||||
|
|
||||||
const changeChart = () => {
|
const changeChart = () => {
|
||||||
message.info('正在开发中,敬请期待');
|
onChangeChart();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToDashboard = () => {
|
const addToDashboard = () => {
|
||||||
@@ -22,32 +51,73 @@ const Tools: React.FC<Props> = ({ data, isLastMessage, isMobileMode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const like = () => {
|
const like = () => {
|
||||||
message.info('正在开发中,敬请期待');
|
setScore(5);
|
||||||
|
updateQAFeedback(queryId, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dislike = () => {
|
const dislike = () => {
|
||||||
message.info('正在开发中,敬请期待');
|
setScore(1);
|
||||||
|
updateQAFeedback(queryId, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const feedbackSection = isLastMessage && (
|
const switchEntity = (option: string) => {
|
||||||
<div className={`${prefixCls}-feedback`}>
|
setRecommendOptionsOpen(false);
|
||||||
<div>这个回答正确吗?</div>
|
onSwitchEntity(option);
|
||||||
<LikeOutlined className={`${prefixCls}-like`} onClick={like} />
|
};
|
||||||
<DislikeOutlined className={`${prefixCls}-dislike`} onClick={dislike} />
|
|
||||||
</div>
|
const likeClass = classNames(`${prefixCls}-like`, {
|
||||||
);
|
[`${prefixCls}-feedback-active`]: score === 5,
|
||||||
|
});
|
||||||
|
const dislikeClass = classNames(`${prefixCls}-dislike`, {
|
||||||
|
[`${prefixCls}-feedback-active`]: score === 1,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={prefixCls}>
|
<div className={prefixCls}>
|
||||||
{!isMobile && !isMobileMode && (
|
{/* {isLastMessage && chatContext?.domainId && entityInfo?.entityId && (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<RecommendOptions
|
||||||
|
entityId={entityInfo.entityId}
|
||||||
|
domainId={chatContext.domainId}
|
||||||
|
domainName={chatContext.domainName}
|
||||||
|
isMobileMode={isMobileMode}
|
||||||
|
onSelect={switchEntity}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement={isMobileMode ? 'top' : 'right'}
|
||||||
|
trigger="click"
|
||||||
|
open={recommendOptionsOpen}
|
||||||
|
onOpenChange={open => setRecommendOptionsOpen(open)}
|
||||||
|
>
|
||||||
|
<Button shape="round">切换其他匹配内容</Button>
|
||||||
|
</Popover>
|
||||||
|
)} */}
|
||||||
|
{!isMobile && (
|
||||||
<>
|
<>
|
||||||
<Button shape="round" onClick={changeChart}>
|
{queryMode === 'METRIC_FILTER' && (
|
||||||
切换图表
|
<Button shape="round" onClick={changeChart}>
|
||||||
</Button>
|
切换图表
|
||||||
<Button shape="round" onClick={addToDashboard}>
|
</Button>
|
||||||
加入看板
|
)}
|
||||||
</Button>
|
{!noDashboard && (
|
||||||
{feedbackSection}
|
<Button shape="round" onClick={addToDashboard}>
|
||||||
|
加入看板
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isLastMessage && (
|
||||||
|
<div className={`${prefixCls}-feedback`}>
|
||||||
|
<div>这个回答正确吗?</div>
|
||||||
|
<LikeOutlined className={likeClass} onClick={like} />
|
||||||
|
<DislikeOutlined
|
||||||
|
className={dislikeClass}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dislike();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-feedback-active {
|
||||||
|
color: rgb(234, 197, 79);
|
||||||
|
}
|
||||||
|
|
||||||
&-mobile-tools {
|
&-mobile-tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -34,4 +38,20 @@
|
|||||||
&-feedback {
|
&-feedback {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-feedback-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-feedback-item-title {
|
||||||
|
width: 40px;
|
||||||
|
margin-right: 20px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const Chat = () => {
|
|||||||
msg={msg}
|
msg={msg}
|
||||||
// msgData={data}
|
// msgData={data}
|
||||||
onMsgDataLoaded={onMsgDataLoaded}
|
onMsgDataLoaded={onMsgDataLoaded}
|
||||||
followQuestions={followQuestions}
|
domainId={37}
|
||||||
isLastMessage
|
isLastMessage
|
||||||
isMobileMode
|
isMobileMode
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export type {
|
|||||||
ChatContextType,
|
ChatContextType,
|
||||||
MsgValidTypeEnum,
|
MsgValidTypeEnum,
|
||||||
MsgDataType,
|
MsgDataType,
|
||||||
InstructionResonseType,
|
PluginResonseType,
|
||||||
ColumnType,
|
ColumnType,
|
||||||
SuggestionItemType,
|
SuggestionItemType,
|
||||||
SuggestionType,
|
SuggestionType,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const axiosInstance: AxiosInstance = axios.create({
|
|||||||
// 设置基本URL,所有请求都会使用这个URL作为前缀
|
// 设置基本URL,所有请求都会使用这个URL作为前缀
|
||||||
baseURL: '',
|
baseURL: '',
|
||||||
// 设置请求超时时间(毫秒)
|
// 设置请求超时时间(毫秒)
|
||||||
timeout: 30000,
|
timeout: 60000,
|
||||||
// 设置请求头
|
// 设置请求头
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import axios from './axiosInstance';
|
import axios from './axiosInstance';
|
||||||
import { ChatContextType, DrillDownDimensionType, HistoryType, MsgDataType, SearchRecommendItem } from '../common/type';
|
import { ChatContextType, DrillDownDimensionType, HistoryType, MsgDataType, ParseDataType, SearchRecommendItem } from '../common/type';
|
||||||
import { QueryDataType } from '../common/type';
|
import { QueryDataType } from '../common/type';
|
||||||
|
|
||||||
const DEFAULT_CHAT_ID = 999;
|
const DEFAULT_CHAT_ID = 0;
|
||||||
|
|
||||||
const prefix = '/api';
|
const prefix = '/api';
|
||||||
|
|
||||||
@@ -14,12 +14,41 @@ export function searchRecommend(queryText: string, chatId?: number, domainId?: n
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function chatQuery(queryText: string, chatId?: number, domainId?: number, isSaveQuestionAnswer?: boolean) {
|
export function chatQuery(queryText: string, chatId?: number, domainId?: number, filters?: any[]) {
|
||||||
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/query`, {
|
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/query`, {
|
||||||
queryText,
|
queryText,
|
||||||
chatId: chatId || DEFAULT_CHAT_ID,
|
chatId: chatId || DEFAULT_CHAT_ID,
|
||||||
domainId,
|
domainId,
|
||||||
isSaveQuestionAnswer
|
queryFilters: filters ? {
|
||||||
|
filters
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chatParse(queryText: string, chatId?: number, domainId?: number, filters?: any[]) {
|
||||||
|
return axios.post<Result<ParseDataType>>(`${prefix}/chat/query/parse`, {
|
||||||
|
queryText,
|
||||||
|
chatId: chatId || DEFAULT_CHAT_ID,
|
||||||
|
domainId,
|
||||||
|
queryFilters: filters ? {
|
||||||
|
filters
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chatExecute(queryText: string, chatId: number, parseInfo: ChatContextType ) {
|
||||||
|
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/execute`, {
|
||||||
|
queryText,
|
||||||
|
chatId: chatId || DEFAULT_CHAT_ID,
|
||||||
|
parseInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchEntity(entityId: string, domainId?: number, chatId?: number) {
|
||||||
|
return axios.post<Result<any>>(`${prefix}/chat/query/switchQuery`, {
|
||||||
|
queryText: entityId,
|
||||||
|
domainId,
|
||||||
|
chatId: chatId || DEFAULT_CHAT_ID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +105,10 @@ export function queryEntities(entityId: string | number, domainId: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateQAFeedback(questionId: number, score: number) {
|
||||||
|
return axios.post<Result<any>>(`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`);
|
||||||
|
}
|
||||||
|
|
||||||
export function queryDrillDownDimensions(domainId: number) {
|
export function queryDrillDownDimensions(domainId: number) {
|
||||||
return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${domainId}`);
|
return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${domainId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,12 @@
|
|||||||
|
|
||||||
@import "../components/ChatMsg/NoPermissionChart/style.less";
|
@import "../components/ChatMsg/NoPermissionChart/style.less";
|
||||||
|
|
||||||
@import "../components/ChatMsg/SemanticInfoPopover/style.less";
|
|
||||||
|
|
||||||
@import "../components/ChatMsg/FilterSection/style.less";
|
@import "../components/ChatMsg/FilterSection/style.less";
|
||||||
|
|
||||||
@import '../components/ChatItem/style.less';
|
@import '../components/ChatItem/style.less';
|
||||||
|
|
||||||
@import "../components/Tools/style.less";
|
@import "../components/Tools/style.less";
|
||||||
|
|
||||||
@import "../components/Suggestion/style.less";
|
@import "../components/RecommendOptions/style.less";
|
||||||
|
|
||||||
@import "../components/DrillDownDimensions/style.less";
|
@import "../components/DrillDownDimensions/style.less";
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export function formatByThousandSeperator(value: number | string) {
|
|||||||
return partValues.join('.');
|
return partValues.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatMetric(value: number | string) {
|
||||||
|
return formatByThousandSeperator(formatByDecimalPlaces(value, 4));
|
||||||
|
}
|
||||||
|
|
||||||
export function formatByUnit(value: number | string, unit: NumericUnit) {
|
export function formatByUnit(value: number | string, unit: NumericUnit) {
|
||||||
const numericValue = +value;
|
const numericValue = +value;
|
||||||
if (isNaN(numericValue) || unit === NumericUnit.None) {
|
if (isNaN(numericValue) || unit === NumericUnit.None) {
|
||||||
@@ -159,6 +163,11 @@ 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 isProd() {
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
}
|
||||||
|
|
||||||
export function setToken(token: string) {
|
export function setToken(token: string) {
|
||||||
localStorage.setItem('SUPERSONIC_CHAT_TOKEN', token);
|
localStorage.setItem('SUPERSONIC_CHAT_TOKEN', token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import ENV_CONFIG from './envConfig';
|
|||||||
const { REACT_APP_ENV, RUN_TYPE } = process.env;
|
const { REACT_APP_ENV, RUN_TYPE } = process.env;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
webpack5: {},
|
||||||
|
mfsu: {},
|
||||||
define: {
|
define: {
|
||||||
// 添加这个自定义的环境变量
|
// 添加这个自定义的环境变量
|
||||||
// 'process.env.REACT_APP_ENV': process.env.REACT_APP_ENV, // * REACT_APP_ENV 本地开发环境:dev,测试服:test,正式服:prod
|
// 'process.env.REACT_APP_ENV': process.env.REACT_APP_ENV, // * REACT_APP_ENV 本地开发环境:dev,测试服:test,正式服:prod
|
||||||
|
|||||||
@@ -8,28 +8,36 @@ const ENV_KEY = {
|
|||||||
const { APP_TARGET } = process.env;
|
const { APP_TARGET } = process.env;
|
||||||
|
|
||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
...(APP_TARGET !== 'inner'
|
{
|
||||||
? [
|
path: '/chat',
|
||||||
{
|
name: 'chat',
|
||||||
path: '/chat',
|
component: './Chat',
|
||||||
name: 'chat',
|
envEnableList: [ENV_KEY.CHAT],
|
||||||
component: './Chat',
|
},
|
||||||
envEnableList: [ENV_KEY.CHAT],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
path: '/chatSetting/:modelId?/:menuKey?',
|
path: '/chatSetting/:modelId?/:menuKey?',
|
||||||
name: 'chatSetting',
|
name: 'chatSetting',
|
||||||
component: './SemanticModel/ChatSetting',
|
component: './SemanticModel/ChatSetting',
|
||||||
envEnableList: [ENV_KEY.CHAT],
|
envEnableList: [ENV_KEY.CHAT],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/chatPlugin',
|
||||||
|
name: 'chatPlugin',
|
||||||
|
component: './ChatPlugin',
|
||||||
|
envEnableList: [ENV_KEY.CHAT],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/semanticModel/:modelId?/:menuKey?',
|
path: '/semanticModel/:modelId?/:menuKey?',
|
||||||
name: 'semanticModel',
|
name: 'semanticModel',
|
||||||
component: './SemanticModel/ProjectManager',
|
component: './SemanticModel/ProjectManager',
|
||||||
envEnableList: [ENV_KEY.SEMANTIC],
|
envEnableList: [ENV_KEY.SEMANTIC],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/Metric',
|
||||||
|
name: 'metric',
|
||||||
|
component: './SemanticModel/Metric',
|
||||||
|
envEnableList: [ENV_KEY.SEMANTIC],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
|||||||
@@ -96,8 +96,8 @@
|
|||||||
"react-split-pane": "^2.0.3",
|
"react-split-pane": "^2.0.3",
|
||||||
"react-syntax-highlighter": "^15.4.3",
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
"sql-formatter": "^2.3.3",
|
"sql-formatter": "^2.3.3",
|
||||||
"supersonic-chat-sdk": "^0.0.0",
|
"supersonic-chat-sdk": "^0.3.0",
|
||||||
"umi": "^3.2.14",
|
"umi": "3.5",
|
||||||
"umi-request": "^1.0.8"
|
"umi-request": "^1.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -145,4 +145,4 @@
|
|||||||
"@types/react": "17.0.0"
|
"@types/react": "17.0.0"
|
||||||
},
|
},
|
||||||
"__npminstall_done": false
|
"__npminstall_done": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ export async function getInitialState(): Promise<{
|
|||||||
await getToken();
|
await getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUser = await fetchUserInfo();
|
let currentUser: any;
|
||||||
|
if (!window.location.pathname.includes('login')) {
|
||||||
|
currentUser = await fetchUserInfo();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
localStorage.setItem('user', currentUser.staffName);
|
localStorage.setItem('user', currentUser.staffName);
|
||||||
|
|||||||
@@ -17,11 +17,3 @@ export enum NumericUnit {
|
|||||||
Million = 'M',
|
Million = 'M',
|
||||||
Giga = 'G',
|
Giga = 'G',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
|
|
||||||
|
|
||||||
export const PAGE_TITLE = '问答对话';
|
|
||||||
|
|
||||||
export const WEB_TITLE = '问答对话 - 超音数';
|
|
||||||
|
|
||||||
export const PLACE_HOLDER = '请输入您的问题';
|
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ ol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-header {
|
.ant-layout-header {
|
||||||
background: linear-gradient(to right, #153d8f, #0a276d);
|
background: linear-gradient(to right, #153d8f, #0a276d) !important;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2) !important;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -242,15 +242,16 @@ ol {
|
|||||||
.ant-tag {
|
.ant-tag {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.semantic-graph-toolbar {
|
.semantic-graph-toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 200px;
|
width: 190px;
|
||||||
|
height: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.g6-component-tooltip {
|
.g6-component-tooltip {
|
||||||
p {
|
p {
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export default {
|
|||||||
'menu.exception.not-find': '404',
|
'menu.exception.not-find': '404',
|
||||||
'menu.exception.server-error': '500',
|
'menu.exception.server-error': '500',
|
||||||
'menu.semanticModel': '模型管理',
|
'menu.semanticModel': '模型管理',
|
||||||
|
'menu.metric': '指标市场',
|
||||||
'menu.chatSetting': '问答设置',
|
'menu.chatSetting': '问答设置',
|
||||||
|
'menu.chatPlugin': '问答插件',
|
||||||
'menu.login': '登录',
|
'menu.login': '登录',
|
||||||
'menu.chat': '问答对话',
|
'menu.chat': '问答对话',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import IconFont from '@/components/IconFont';
|
import IconFont from '@/components/IconFont';
|
||||||
import { getTextWidth, groupByColumn } from '@/utils/utils';
|
import { getTextWidth, groupByColumn, isMobile } from '@/utils/utils';
|
||||||
import { AutoComplete, Select, Tag, Tooltip } 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';
|
||||||
@@ -9,20 +9,23 @@ 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 '../constants';
|
import { PLACE_HOLDER } from '../constants';
|
||||||
import { DomainType } from '../type';
|
import { DefaultEntityType, DomainType } from '../type';
|
||||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inputMsg: string;
|
inputMsg: string;
|
||||||
chatId?: number;
|
chatId?: number;
|
||||||
currentDomain?: DomainType;
|
currentDomain?: DomainType;
|
||||||
|
defaultEntity?: DefaultEntityType;
|
||||||
|
isCopilotMode?: boolean;
|
||||||
|
copilotFullscreen?: boolean;
|
||||||
domains: DomainType[];
|
domains: DomainType[];
|
||||||
isMobileMode?: boolean;
|
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
onToggleCollapseBtn: () => void;
|
onToggleCollapseBtn: () => void;
|
||||||
onInputMsgChange: (value: string) => void;
|
onInputMsgChange: (value: string) => void;
|
||||||
onSendMsg: (msg: string, domainId?: number) => void;
|
onSendMsg: (msg: string, domainId?: number) => void;
|
||||||
onAddConversation: () => void;
|
onAddConversation: () => void;
|
||||||
|
onCancelDefaultFilter: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { OptGroup, Option } = Select;
|
const { OptGroup, Option } = Select;
|
||||||
@@ -42,13 +45,16 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
inputMsg,
|
inputMsg,
|
||||||
chatId,
|
chatId,
|
||||||
currentDomain,
|
currentDomain,
|
||||||
|
defaultEntity,
|
||||||
domains,
|
domains,
|
||||||
isMobileMode,
|
|
||||||
collapsed,
|
collapsed,
|
||||||
|
isCopilotMode,
|
||||||
|
copilotFullscreen,
|
||||||
onToggleCollapseBtn,
|
onToggleCollapseBtn,
|
||||||
onInputMsgChange,
|
onInputMsgChange,
|
||||||
onSendMsg,
|
onSendMsg,
|
||||||
onAddConversation,
|
onAddConversation,
|
||||||
|
onCancelDefaultFilter,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@@ -95,7 +101,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
|
|
||||||
const getStepOptions = (recommends: any[]) => {
|
const getStepOptions = (recommends: any[]) => {
|
||||||
const data = groupByColumn(recommends, 'domainName');
|
const data = groupByColumn(recommends, 'domainName');
|
||||||
return isMobileMode && recommends.length > 6
|
return isMobile && recommends.length > 6
|
||||||
? Object.keys(data)
|
? Object.keys(data)
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
.reduce((result, key) => {
|
.reduce((result, key) => {
|
||||||
@@ -135,7 +141,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
fetchRef.current += 1;
|
fetchRef.current += 1;
|
||||||
const fetchId = fetchRef.current;
|
const fetchId = fetchRef.current;
|
||||||
const { msgValue, domainId } = processMsg(msg, domains);
|
const { msgValue, domainId } = processMsg(msg, domains);
|
||||||
const res = await searchRecommend(msgValue.trim(), chatId, domainId || domain?.id);
|
const domainIdValue = domainId || domain?.id;
|
||||||
|
const res = await searchRecommend(msgValue.trim(), chatId, domainIdValue);
|
||||||
if (fetchId !== fetchRef.current) {
|
if (fetchId !== fetchRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,7 +157,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
}
|
}
|
||||||
setOpen(recommends.length > 0);
|
setOpen(recommends.length > 0);
|
||||||
};
|
};
|
||||||
return debounce(getAssociateWords, 20);
|
return debounce(getAssociateWords, 200);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
|
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
|
||||||
@@ -222,7 +229,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
|
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
|
||||||
[styles.mobile]: isMobileMode,
|
[styles.mobile]: isMobile,
|
||||||
[styles.domainOptions]: domainOptions.length > 0,
|
[styles.domainOptions]: domainOptions.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +245,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chatFooterClass = classNames(styles.chatFooter, {
|
const chatFooterClass = classNames(styles.chatFooter, {
|
||||||
[styles.mobile]: isMobileMode,
|
[styles.mobile]: isMobile,
|
||||||
|
[styles.defaultCopilotMode]: isCopilotMode && !copilotFullscreen,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -255,6 +263,33 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className={styles.composerInputWrapper}>
|
<div className={styles.composerInputWrapper}>
|
||||||
|
{currentDomain && (
|
||||||
|
<div className={styles.currentDomain}>
|
||||||
|
<div className={styles.currentDomainName}>
|
||||||
|
输入联想与问题回复将限定于:“
|
||||||
|
<span className={styles.quoteText}>
|
||||||
|
主题域【{currentDomain.name}】
|
||||||
|
{defaultEntity && (
|
||||||
|
<>
|
||||||
|
<span>,</span>
|
||||||
|
<span>{`${currentDomain.name.slice(
|
||||||
|
0,
|
||||||
|
currentDomain.name.length - 1,
|
||||||
|
)}【`}</span>
|
||||||
|
<span className={styles.entityName} title={defaultEntity.entityName}>
|
||||||
|
{defaultEntity.entityName}
|
||||||
|
</span>
|
||||||
|
<span>】</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
”
|
||||||
|
</div>
|
||||||
|
<div className={styles.cancelDomain} onClick={onCancelDefaultFilter}>
|
||||||
|
取消限定
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
className={styles.composerInput}
|
className={styles.composerInput}
|
||||||
placeholder={
|
placeholder={
|
||||||
@@ -265,7 +300,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
value={inputMsg}
|
value={inputMsg}
|
||||||
onChange={onInputMsgChange}
|
onChange={onInputMsgChange}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
autoFocus={!isMobileMode}
|
autoFocus={!isMobile}
|
||||||
backfill
|
backfill
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id="chatInput"
|
id="chatInput"
|
||||||
@@ -332,7 +367,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
|||||||
? 'blue'
|
? 'blue'
|
||||||
: option.schemaElementType === SemanticTypeEnum.VALUE
|
: option.schemaElementType === SemanticTypeEnum.VALUE
|
||||||
? 'geekblue'
|
? 'geekblue'
|
||||||
: 'orange'
|
: 'cyan'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
|
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
&.defaultCopilotMode {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.composer {
|
.composer {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
@@ -38,8 +42,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.composerInputWrapper {
|
.composerInputWrapper {
|
||||||
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
.currentDomain {
|
||||||
|
position: absolute;
|
||||||
|
top: -30px;
|
||||||
|
left: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
white-space: nowrap;
|
||||||
|
background: #f4f6f5;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
|
||||||
|
.currentDomainName {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.entityName {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelDomain {
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--text-color-fourth);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
border-color: var(--text-color-fifth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.composerInput {
|
.composerInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -191,3 +240,7 @@
|
|||||||
.semanticType {
|
.semanticType {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quoteText {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,17 +9,22 @@ import {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useLocation } from 'umi';
|
import { useLocation } from 'umi';
|
||||||
import ConversationModal from './ConversationModal';
|
import ConversationModal from './components/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, DefaultEntityType } from './type';
|
||||||
|
import { DEFAULT_CONVERSATION_NAME } from './constants';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { DEFAULT_CONVERSATION_NAME } from '@/common/constants';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentConversation?: ConversationDetailType;
|
currentConversation?: ConversationDetailType;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
|
isCopilotMode?: boolean;
|
||||||
|
defaultDomainName?: string;
|
||||||
|
defaultEntityFilter?: DefaultEntityType;
|
||||||
|
triggerNewConversation?: boolean;
|
||||||
|
onNewConversationTriggered?: () => void;
|
||||||
onSelectConversation: (
|
onSelectConversation: (
|
||||||
conversation: ConversationDetailType,
|
conversation: ConversationDetailType,
|
||||||
name?: string,
|
name?: string,
|
||||||
@@ -29,7 +34,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||||
{ currentConversation, collapsed, onSelectConversation },
|
{
|
||||||
|
currentConversation,
|
||||||
|
collapsed,
|
||||||
|
isCopilotMode,
|
||||||
|
defaultDomainName,
|
||||||
|
defaultEntityFilter,
|
||||||
|
triggerNewConversation,
|
||||||
|
onNewConversationTriggered,
|
||||||
|
onSelectConversation,
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -47,7 +61,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
|||||||
const updateData = async () => {
|
const updateData = async () => {
|
||||||
const { data } = await getAllConversations();
|
const { data } = await getAllConversations();
|
||||||
const conversationList = data || [];
|
const conversationList = data || [];
|
||||||
setConversations(conversationList);
|
setConversations(conversationList.slice(0, 500));
|
||||||
return conversationList;
|
return conversationList;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,6 +85,20 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (triggerNewConversation) {
|
||||||
|
const conversationName =
|
||||||
|
defaultEntityFilter?.entityName && window.location.pathname.includes('detail')
|
||||||
|
? defaultEntityFilter.entityName
|
||||||
|
: defaultDomainName;
|
||||||
|
onAddConversation({ name: conversationName, type: 'CUSTOMIZE' });
|
||||||
|
onNewConversationTriggered?.();
|
||||||
|
}
|
||||||
|
}, [triggerNewConversation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerNewConversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (q && cid === undefined && window.location.href.includes('/workbench/chat')) {
|
if (q && cid === undefined && window.location.href.includes('/workbench/chat')) {
|
||||||
onAddConversation({ name: q, domainId: domainId ? +domainId : undefined, entityId });
|
onAddConversation({ name: q, domainId: domainId ? +domainId : undefined, entityId });
|
||||||
} else {
|
} else {
|
||||||
@@ -114,6 +142,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
|||||||
|
|
||||||
const conversationClass = classNames(styles.conversation, {
|
const conversationClass = classNames(styles.conversation, {
|
||||||
[styles.collapsed]: collapsed,
|
[styles.collapsed]: collapsed,
|
||||||
|
[styles.copilotMode]: isCopilotMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const convertTime = (date: string) => {
|
const convertTime = (date: string) => {
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
.conversation {
|
|
||||||
position: relative;
|
|
||||||
width: 260px;
|
|
||||||
height: 100vh !important;
|
|
||||||
background-color: #fff;
|
|
||||||
border-right: 1px solid var(--border-color-base);
|
|
||||||
|
|
||||||
.leftSection {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.searchConversation {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 9px 10px;
|
|
||||||
|
|
||||||
.searchIcon {
|
|
||||||
color: #999 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchTask {
|
|
||||||
font-size: 13px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: none !important;
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.ant-input {
|
|
||||||
font-size: 13px !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversationList {
|
|
||||||
height: calc(100vh - 50px);
|
|
||||||
padding: 2px 8px 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
.conversationItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 2px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.conversationIcon {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversationContent {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.topTitleBar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.conversationName {
|
|
||||||
width: 150px;
|
|
||||||
margin-right: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversationTime {
|
|
||||||
color: var(--text-color-six);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
width: 180px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-color-six);
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.activeConversationItem {
|
|
||||||
background-color: var(--light-blue-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--light-background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.operateSection {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operateItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.operateIcon {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--text-color-fourth);
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operateLabel {
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.operateLabel {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
width: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-right: 0;
|
|
||||||
|
|
||||||
.leftSection {
|
|
||||||
.searchConversation {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversationList {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operateSection {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,9 @@ import { isEqual } from 'lodash';
|
|||||||
import { ChatItem } from 'supersonic-chat-sdk';
|
import { ChatItem } from 'supersonic-chat-sdk';
|
||||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||||
import { MessageItem, MessageTypeEnum } from './type';
|
import { MessageItem, MessageTypeEnum } from './type';
|
||||||
|
import Plugin from './components/Plugin';
|
||||||
|
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import RecommendQuestions from './components/RecommendQuestions';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,10 +14,16 @@ type Props = {
|
|||||||
messageList: MessageItem[];
|
messageList: MessageItem[];
|
||||||
isMobileMode?: boolean;
|
isMobileMode?: boolean;
|
||||||
conversationCollapsed: boolean;
|
conversationCollapsed: boolean;
|
||||||
|
copilotFullscreen?: boolean;
|
||||||
onClickMessageContainer: () => void;
|
onClickMessageContainer: () => void;
|
||||||
onMsgDataLoaded: (data: MsgDataType, questionId: string | number) => void;
|
onMsgDataLoaded: (
|
||||||
onSelectSuggestion: (value: string) => void;
|
data: MsgDataType,
|
||||||
|
questionId: string | number,
|
||||||
|
question: string,
|
||||||
|
valid: boolean,
|
||||||
|
) => void;
|
||||||
onCheckMore: (data: MsgDataType) => void;
|
onCheckMore: (data: MsgDataType) => void;
|
||||||
|
onApplyAuth: (domain: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageContainer: React.FC<Props> = ({
|
const MessageContainer: React.FC<Props> = ({
|
||||||
@@ -25,9 +32,11 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
messageList,
|
messageList,
|
||||||
isMobileMode,
|
isMobileMode,
|
||||||
conversationCollapsed,
|
conversationCollapsed,
|
||||||
|
copilotFullscreen,
|
||||||
onClickMessageContainer,
|
onClickMessageContainer,
|
||||||
onMsgDataLoaded,
|
onMsgDataLoaded,
|
||||||
onSelectSuggestion,
|
onCheckMore,
|
||||||
|
onApplyAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [triggerResize, setTriggerResize] = useState(false);
|
const [triggerResize, setTriggerResize] = useState(false);
|
||||||
|
|
||||||
@@ -38,10 +47,6 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onResize();
|
|
||||||
}, [conversationCollapsed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -49,6 +54,15 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onResize();
|
||||||
|
}, [conversationCollapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onResize();
|
||||||
|
updateMessageContainerScroll();
|
||||||
|
}, [copilotFullscreen]);
|
||||||
|
|
||||||
const getFollowQuestions = (index: number) => {
|
const getFollowQuestions = (index: number) => {
|
||||||
const followQuestions: string[] = [];
|
const followQuestions: string[] = [];
|
||||||
const currentMsg = messageList[index];
|
const currentMsg = messageList[index];
|
||||||
@@ -63,7 +77,7 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
const currentMsgEntityId = currentMsgData?.entityInfo?.entityId;
|
const currentMsgEntityId = currentMsgData?.entityInfo?.entityId;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.INSTRUCTION) &&
|
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.PLUGIN) &&
|
||||||
!!currentMsgDomainId &&
|
!!currentMsgDomainId &&
|
||||||
msgDomainId === currentMsgDomainId &&
|
msgDomainId === currentMsgDomainId &&
|
||||||
msgEntityId === currentMsgEntityId &&
|
msgEntityId === currentMsgEntityId &&
|
||||||
@@ -77,19 +91,38 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
return followQuestions;
|
return followQuestions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFilters = (domainId?: number, entityId?: string) => {
|
||||||
|
if (!domainId || !entityId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: entityId,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
||||||
<div className={styles.messageList}>
|
<div className={styles.messageList}>
|
||||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||||
const { id: msgId, domainId, type, msg, msgValue, identityMsg, msgData } = msgItem;
|
const {
|
||||||
|
id: msgId,
|
||||||
|
domainId,
|
||||||
|
entityId,
|
||||||
|
type,
|
||||||
|
msg,
|
||||||
|
msgValue,
|
||||||
|
identityMsg,
|
||||||
|
msgData,
|
||||||
|
score,
|
||||||
|
isHistory,
|
||||||
|
} = msgItem;
|
||||||
|
|
||||||
const followQuestions = getFollowQuestions(index);
|
const followQuestions = getFollowQuestions(index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
||||||
{type === MessageTypeEnum.RECOMMEND_QUESTIONS && (
|
|
||||||
<RecommendQuestions onSelectQuestion={onSelectSuggestion} />
|
|
||||||
)}
|
|
||||||
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
||||||
{type === MessageTypeEnum.QUESTION && (
|
{type === MessageTypeEnum.QUESTION && (
|
||||||
<>
|
<>
|
||||||
@@ -97,16 +130,35 @@ const MessageContainer: React.FC<Props> = ({
|
|||||||
{identityMsg && <Text position="left" data={identityMsg} />}
|
{identityMsg && <Text position="left" data={identityMsg} />}
|
||||||
<ChatItem
|
<ChatItem
|
||||||
msg={msgValue || msg || ''}
|
msg={msgValue || msg || ''}
|
||||||
followQuestions={followQuestions}
|
|
||||||
msgData={msgData}
|
msgData={msgData}
|
||||||
conversationId={chatId}
|
conversationId={chatId}
|
||||||
domainId={domainId}
|
domainId={domainId}
|
||||||
|
filter={getFilters(domainId, entityId)}
|
||||||
isLastMessage={index === messageList.length - 1}
|
isLastMessage={index === messageList.length - 1}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
triggerResize={triggerResize}
|
triggerResize={triggerResize}
|
||||||
onMsgDataLoaded={(data: MsgDataType) => {
|
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||||
onMsgDataLoaded(data, msgId);
|
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
|
||||||
}}
|
}}
|
||||||
|
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === MessageTypeEnum.PLUGIN && (
|
||||||
|
<>
|
||||||
|
<Plugin
|
||||||
|
id={msgId}
|
||||||
|
followQuestions={followQuestions}
|
||||||
|
data={msgData!}
|
||||||
|
scoreValue={score}
|
||||||
|
msg={msgValue || msg || ''}
|
||||||
|
isHistory={isHistory}
|
||||||
|
isLastMessage={index === messageList.length - 1}
|
||||||
|
isMobileMode={isMobileMode}
|
||||||
|
onReportLoaded={(height: number) => {
|
||||||
|
updateMessageContainerScroll(true, height);
|
||||||
|
}}
|
||||||
|
onCheckMore={onCheckMore}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -122,7 +174,8 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
|||||||
if (
|
if (
|
||||||
prevProps.id === nextProps.id &&
|
prevProps.id === nextProps.id &&
|
||||||
isEqual(prevProps.messageList, nextProps.messageList) &&
|
isEqual(prevProps.messageList, nextProps.messageList) &&
|
||||||
prevProps.conversationCollapsed === nextProps.conversationCollapsed
|
prevProps.conversationCollapsed === nextProps.conversationCollapsed &&
|
||||||
|
prevProps.copilotFullscreen === nextProps.copilotFullscreen
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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,59 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import styles from './style.less';
|
|
||||||
import type { ChatContextType, EntityInfoType } from 'supersonic-chat-sdk';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
chatContext: ChatContextType;
|
|
||||||
entityInfo?: EntityInfoType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Context: React.FC<Props> = ({ chatContext, entityInfo }) => {
|
|
||||||
const { domainName, metrics, dateInfo, dimensionFilters } = chatContext;
|
|
||||||
|
|
||||||
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}>{domainName}</span>
|
|
||||||
</div>
|
|
||||||
{dateInfo && (
|
|
||||||
<div className={styles.field}>
|
|
||||||
<span className={styles.fieldName}>时间范围:</span>
|
|
||||||
<span className={styles.fieldValue}>
|
|
||||||
{dateInfo.text ||
|
|
||||||
`近${moment(dateInfo.endDate).diff(moment(dateInfo.startDate), 'days') + 1}天`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{metrics && metrics.length > 0 && (
|
|
||||||
<div className={styles.field}>
|
|
||||||
<span className={styles.fieldName}>指标:</span>
|
|
||||||
<span className={styles.fieldValue}>
|
|
||||||
{metrics.map((metric) => metric.name).join('、')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dimensionFilters &&
|
|
||||||
dimensionFilters.length > 0 &&
|
|
||||||
!(entityInfo?.dimensions && entityInfo.dimensions.length > 0) && (
|
|
||||||
<div className={styles.filterSection}>
|
|
||||||
<div className={styles.fieldName}>筛选条件:</div>
|
|
||||||
<div className={styles.filterValues}>
|
|
||||||
{dimensionFilters.map((filter) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.filterItem} key={filter.name}>
|
|
||||||
{filter.name}:{filter.value}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Context;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
.context {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px 10px 0;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: 22px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
max-height: 350px;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
overflow-y: auto;
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&.columnLayout {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterSection {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldName {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue {
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-color);
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterValues {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
column-gap: 4px;
|
|
||||||
row-gap: 4px;
|
|
||||||
|
|
||||||
.filterItem {
|
|
||||||
padding: 2px 12px;
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
background-color: var(--body-background);
|
|
||||||
border-radius: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { getFormattedValueData } from '@/utils/utils';
|
|
||||||
import moment from 'moment';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './style.less';
|
|
||||||
import type { EntityInfoType, MsgDataType } from 'supersonic-chat-sdk';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
currentEntity: MsgDataType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Introduction: React.FC<Props> = ({ currentEntity }) => {
|
|
||||||
const { entityInfo } = currentEntity;
|
|
||||||
const { dimensions, metrics } = entityInfo || ({} as EntityInfoType);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.introduction}>
|
|
||||||
{dimensions
|
|
||||||
?.filter((dimension) => !dimension.bizName.includes('photo'))
|
|
||||||
.map((dimension) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.field} key={dimension.name}>
|
|
||||||
<span className={styles.fieldName}>{dimension.name}:</span>
|
|
||||||
{dimension.bizName.includes('photo') ? (
|
|
||||||
<img width={40} height={40} src={dimension.value} alt="" />
|
|
||||||
) : (
|
|
||||||
<span className={styles.fieldValue}>
|
|
||||||
{dimension.bizName.includes('publish_time')
|
|
||||||
? moment(dimension.value).format('YYYY-MM-DD')
|
|
||||||
: dimension.value}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{metrics?.map((metric) => (
|
|
||||||
<div className={styles.field} key={metric.name}>
|
|
||||||
<span className={styles.fieldName}>{metric.name}:</span>
|
|
||||||
<span className={styles.fieldValue}>{getFormattedValueData(metric.value)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Introduction;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
.introduction {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0 10px 4px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: 22px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
max-height: 350px;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
overflow-y: auto;
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&.columnLayout {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldName {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue {
|
|
||||||
color: var(--text-color);
|
|
||||||
|
|
||||||
&.switchField {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dimensionFieldValue {
|
|
||||||
max-width: 90px;
|
|
||||||
// white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mainNameFieldValue {
|
|
||||||
max-width: 90px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import Context from './Context';
|
|
||||||
import Introduction from './Introduction';
|
|
||||||
import styles from './style.less';
|
|
||||||
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 = {
|
|
||||||
domains: DomainType[];
|
|
||||||
currentEntity?: MsgDataType;
|
|
||||||
currentConversation?: ConversationDetailType;
|
|
||||||
currentDomain?: DomainType;
|
|
||||||
conversationRef: any;
|
|
||||||
onSelectConversation: (conversation: ConversationDetailType, name?: string) => void;
|
|
||||||
onSelectDomain: (domain: DomainType) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RightSection: React.FC<Props> = ({
|
|
||||||
domains,
|
|
||||||
currentEntity,
|
|
||||||
currentDomain,
|
|
||||||
currentConversation,
|
|
||||||
conversationRef,
|
|
||||||
onSelectConversation,
|
|
||||||
onSelectDomain,
|
|
||||||
}) => {
|
|
||||||
const rightSectionClass = classNames(styles.rightSection, {
|
|
||||||
[styles.external]: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={rightSectionClass}>
|
|
||||||
<Conversation
|
|
||||||
currentConversation={currentConversation}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
ref={conversationRef}
|
|
||||||
/>
|
|
||||||
{currentDomain && !currentEntity && (
|
|
||||||
<div className={styles.entityInfo}>
|
|
||||||
<DomainInfo domain={currentDomain} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!currentEntity?.chatContext?.domainId && (
|
|
||||||
<div className={styles.entityInfo}>
|
|
||||||
<Context chatContext={currentEntity.chatContext} entityInfo={currentEntity.entityInfo} />
|
|
||||||
<Introduction currentEntity={currentEntity} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{domains && domains.length > 0 && (
|
|
||||||
<Domains domains={domains} currentDomain={currentDomain} onSelectDomain={onSelectDomain} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RightSection;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.rightSection {
|
|
||||||
width: 225px;
|
|
||||||
height: calc(100vh - 48px);
|
|
||||||
margin-right: 12px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.entityInfo {
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
.topInfo {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: var(--text-color-third);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import IconFont from '@/components/IconFont';
|
import IconFont from '@/components/IconFont';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
|
|
||||||
const LeftAvatar = () => {
|
const CopilotAvatar = () => {
|
||||||
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
|
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LeftAvatar;
|
export default CopilotAvatar;
|
||||||
@@ -34,7 +34,14 @@ const Message: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={messageClass} style={{ width }}>
|
<div className={messageClass} style={{ width }}>
|
||||||
{!!domainName && <div className={styles.domainName}>{domainName}</div>}
|
{/* <div className={styles.messageTitleBar}>
|
||||||
|
{!!domainName && <div className={styles.domainName}>{domainName}</div>}
|
||||||
|
{position === 'left' && leftTitle && (
|
||||||
|
<div className={styles.messageTopBar} title={leftTitle}>
|
||||||
|
({leftTitle})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div> */}
|
||||||
<div className={styles.messageContent}>
|
<div className={styles.messageContent}>
|
||||||
<div className={styles.messageBody}>
|
<div className={styles.messageBody}>
|
||||||
<div
|
<div
|
||||||
@@ -44,11 +51,11 @@ const Message: React.FC<Props> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{position === 'left' && question && (
|
{/* {position === 'left' && question && (
|
||||||
<div className={styles.messageTopBar} title={leftTitle}>
|
<div className={styles.messageTopBar} title={leftTitle}>
|
||||||
{leftTitle}
|
{leftTitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const TRANS_30_REPORT_DASHBOARD_ID = '1165';
|
||||||
|
|
||||||
|
export const HEAT_COMPARE_REPORT_DASHBOARD_ID = '1148';
|
||||||
|
|
||||||
|
export const INSIGHTS_DETAIL_ID = 'DETAIL';
|
||||||
|
export const INSIGHTS_ID = 'INSIGHT';
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { isProd } from '@/utils/utils';
|
||||||
|
import { MsgDataType } from 'supersonic-chat-sdk';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import Message from '../Message';
|
||||||
|
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||||
|
import styles from './style.less';
|
||||||
|
import LeftAvatar from '../CopilotAvatar';
|
||||||
|
import { DislikeOutlined, LikeOutlined } from '@ant-design/icons';
|
||||||
|
import { updateQAFeedback } from '../../service';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string | number;
|
||||||
|
followQuestions?: string[];
|
||||||
|
data: MsgDataType;
|
||||||
|
scoreValue?: number;
|
||||||
|
msg: string;
|
||||||
|
isHistory?: boolean;
|
||||||
|
isLastMessage: boolean;
|
||||||
|
isMobileMode?: boolean;
|
||||||
|
onReportLoaded: (height: number) => void;
|
||||||
|
onCheckMore: (data: MsgDataType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_HEIGHT = 800;
|
||||||
|
|
||||||
|
const Plugin: React.FC<Props> = ({
|
||||||
|
id,
|
||||||
|
followQuestions,
|
||||||
|
data,
|
||||||
|
scoreValue,
|
||||||
|
msg,
|
||||||
|
isHistory,
|
||||||
|
isLastMessage,
|
||||||
|
isMobileMode,
|
||||||
|
onReportLoaded,
|
||||||
|
onCheckMore,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
webPage: { url, params },
|
||||||
|
} = data.response || {};
|
||||||
|
|
||||||
|
const [pluginUrl, setPluginUrl] = useState('');
|
||||||
|
const [height, setHeight] = useState(DEFAULT_HEIGHT);
|
||||||
|
const [score, setScore] = useState(scoreValue || 0);
|
||||||
|
|
||||||
|
const handleMessage = useCallback((event: MessageEvent) => {
|
||||||
|
const messageData = event.data;
|
||||||
|
const { type, payload } = messageData;
|
||||||
|
if (type === 'changeMiniProgramContainerSize') {
|
||||||
|
const { msgId, height } = payload;
|
||||||
|
if (`${msgId}` === `${id}`) {
|
||||||
|
setHeight(height);
|
||||||
|
updateMessageContainerScroll();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (messageData === 'storyResize') {
|
||||||
|
const ifr: any = document.getElementById(`reportIframe_${id}`);
|
||||||
|
const iDoc = ifr.contentDocument || ifr.document || ifr.contentWindow;
|
||||||
|
setTimeout(() => {
|
||||||
|
setHeight(isProd() ? calcPageHeight(iDoc) : DEFAULT_HEIGHT);
|
||||||
|
}, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [handleMessage]);
|
||||||
|
|
||||||
|
function calcPageHeight(doc: any) {
|
||||||
|
const titleAreaEl = doc.getElementById('titleArea');
|
||||||
|
const titleAreaHeight = Math.max(
|
||||||
|
titleAreaEl?.clientHeight || 0,
|
||||||
|
titleAreaEl?.scrollHeight || 0,
|
||||||
|
);
|
||||||
|
const dashboardGridEl = doc.getElementsByClassName('dashboardGrid')?.[0];
|
||||||
|
const dashboardGridHeight = Math.max(
|
||||||
|
dashboardGridEl?.clientHeight || 0,
|
||||||
|
dashboardGridEl?.scrollHeight || 0,
|
||||||
|
);
|
||||||
|
return Math.max(titleAreaHeight + dashboardGridHeight + 10, DEFAULT_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initData = () => {
|
||||||
|
const heightValue =
|
||||||
|
params?.find((option: any) => option.paramType === 'FORWARD' && option.key === 'height')
|
||||||
|
?.value || DEFAULT_HEIGHT;
|
||||||
|
setHeight(heightValue);
|
||||||
|
let urlValue = url;
|
||||||
|
const valueParams = (params || [])
|
||||||
|
.filter((option: any) => option.paramType !== 'FORWARD')
|
||||||
|
.reduce((result: any, item: any) => {
|
||||||
|
result[item.key] = item.value;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
if (urlValue.includes('?type=dashboard') || urlValue.includes('?type=widget')) {
|
||||||
|
const filterData = encodeURIComponent(
|
||||||
|
JSON.stringify(
|
||||||
|
urlValue.includes('dashboard')
|
||||||
|
? {
|
||||||
|
global: valueParams,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
local: valueParams,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
urlValue = urlValue.replace(
|
||||||
|
'?',
|
||||||
|
`?miniProgram=true&reportName=${name}&filterData=${filterData}&`,
|
||||||
|
);
|
||||||
|
urlValue =
|
||||||
|
!isProd() && !urlValue.includes('http') ? `http://s2.tmeoa.com${urlValue}` : urlValue;
|
||||||
|
} else {
|
||||||
|
const params = Object.keys(valueParams || {}).map((key) => `${key}=${valueParams[key]}`);
|
||||||
|
if (params.length > 0) {
|
||||||
|
if (url.includes('?')) {
|
||||||
|
urlValue = urlValue.replace('?', `?${params.join('&')}&`);
|
||||||
|
} else {
|
||||||
|
urlValue = `${urlValue}?${params.join('&')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReportLoaded(heightValue + 190);
|
||||||
|
setPluginUrl(urlValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reportClass = classNames(styles.report, {
|
||||||
|
[styles.mobileMode]: isMobileMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const like = () => {
|
||||||
|
setScore(5);
|
||||||
|
updateQAFeedback(data.queryId, 5);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dislike = () => {
|
||||||
|
setScore(1);
|
||||||
|
updateQAFeedback(data.queryId, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const likeClass = classNames(styles.like, {
|
||||||
|
[styles.feedbackActive]: score === 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dislikeClass = classNames(styles.dislike, {
|
||||||
|
[styles.feedbackActive]: score === 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={reportClass}>
|
||||||
|
<LeftAvatar />
|
||||||
|
<div className={styles.msgContent}>
|
||||||
|
<Message
|
||||||
|
position="left"
|
||||||
|
width="100%"
|
||||||
|
height={height}
|
||||||
|
bubbleClassName={styles.reportBubble}
|
||||||
|
domainName={data.chatContext?.domainName}
|
||||||
|
question={msg}
|
||||||
|
followQuestions={followQuestions}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
id={`reportIframe_${id}`}
|
||||||
|
src={pluginUrl}
|
||||||
|
className={styles.reportContent}
|
||||||
|
style={{ height }}
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</Message>
|
||||||
|
{isLastMessage && (
|
||||||
|
<div className={styles.tools}>
|
||||||
|
<div className={styles.feedback}>
|
||||||
|
<div>这个回答正确吗?</div>
|
||||||
|
<LikeOutlined className={likeClass} onClick={like} />
|
||||||
|
<DislikeOutlined className={dislikeClass} onClick={dislike} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Plugin;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.report {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.reportLoading {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgContent {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
column-gap: 4px;
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
column-gap: 6px;
|
||||||
|
|
||||||
|
.like {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbackActive {
|
||||||
|
color: rgb(234, 197, 79);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobileMode {
|
||||||
|
.msgContent {
|
||||||
|
width: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportBubble {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportContent {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveReport {
|
||||||
|
width: fit-content;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid var(--border-color-base);
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import LeftAvatar from '../LeftAvatar';
|
import LeftAvatar from '../CopilotAvatar';
|
||||||
import Message from '../Message';
|
import Message from '../Message';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import { queryRecommendQuestions } from '../../service';
|
import { queryRecommendQuestions } from '../../service';
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { querySuggestion } from '../../service';
|
|
||||||
import { SuggestionType } from '../../type';
|
|
||||||
import Message from '../Message';
|
|
||||||
import styles from './style.less';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
domainId: number;
|
|
||||||
onSelectSuggestion: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Suggestion: React.FC<Props> = ({ domainId, onSelectSuggestion }) => {
|
|
||||||
const [data, setData] = useState<SuggestionType>({ dimensions: [], metrics: [] });
|
|
||||||
const { metrics } = data;
|
|
||||||
|
|
||||||
const initData = async () => {
|
|
||||||
const res = await querySuggestion(domainId);
|
|
||||||
setData({
|
|
||||||
dimensions: res.data.dimensions.slice(0, 5),
|
|
||||||
metrics: res.data.metrics.slice(0, 5),
|
|
||||||
});
|
|
||||||
updateMessageContainerScroll();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.suggestion}>
|
|
||||||
<Message position="left" bubbleClassName={styles.suggestionMsg}>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<div className={styles.rowTitle}>您可能还想问以下指标:</div>
|
|
||||||
<div className={styles.rowContent}>
|
|
||||||
{metrics.map((metric, index) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={styles.contentItemName}
|
|
||||||
onClick={() => {
|
|
||||||
onSelectSuggestion(metric.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{metric.name}
|
|
||||||
</span>
|
|
||||||
{index !== metrics.length - 1 && <span>、</span>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Message>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Suggestion;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
.suggestion {
|
|
||||||
margin-left: 46px;
|
|
||||||
|
|
||||||
.suggestionMsg {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 16px !important;
|
|
||||||
row-gap: 12px;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 4px;
|
|
||||||
row-gap: 12px;
|
|
||||||
|
|
||||||
.rowTitle {
|
|
||||||
color: var(--text-color-third);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowContent {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-color);
|
|
||||||
row-gap: 12px;
|
|
||||||
|
|
||||||
.contentItemName {
|
|
||||||
color: var(--chat-blue);
|
|
||||||
font-weight: 500;
|
|
||||||
border-bottom: 1px solid var(--chat-blue);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import LeftAvatar from './LeftAvatar';
|
import LeftAvatar from './CopilotAvatar';
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
.message {
|
.message {
|
||||||
.domainName {
|
.messageTitleBar {
|
||||||
margin-bottom: 2px;
|
display: flex;
|
||||||
margin-left: 4px;
|
align-items: baseline;
|
||||||
color: var(--text-color);
|
margin-bottom: 6px;
|
||||||
font-weight: 500;
|
column-gap: 10px;
|
||||||
|
|
||||||
|
.domainName {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageTopBar {
|
||||||
|
position: relative;
|
||||||
|
max-width: 80%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-color-third);
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.messageContent {
|
.messageContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -11,18 +27,6 @@
|
|||||||
|
|
||||||
.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 {
|
||||||
@@ -88,7 +92,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
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;
|
||||||
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);
|
||||||
@@ -229,7 +233,6 @@
|
|||||||
|
|
||||||
.typingBubble {
|
.typingBubble {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 16px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote {
|
.quote {
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ export const SEMANTIC_TYPE_MAP = {
|
|||||||
[SemanticTypeEnum.VALUE]: '维度值',
|
[SemanticTypeEnum.VALUE]: '维度值',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话'
|
export const CHAT_TITLE = '';
|
||||||
|
|
||||||
export const WEB_TITLE = '问答对话'
|
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
|
||||||
|
|
||||||
export const CHAT_TITLE = '问答'
|
export const PAGE_TITLE = '问答对话';
|
||||||
|
|
||||||
export const PLACE_HOLDER = '请输入您的问题'
|
export const WEB_TITLE = '问答对话';
|
||||||
|
|
||||||
|
export const PLACE_HOLDER = '请输入您的问题';
|
||||||
|
|||||||
@@ -1,28 +1,54 @@
|
|||||||
import { updateMessageContainerScroll, isMobile, uuid, getLeafList } 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, useDispatch, useLocation } from 'umi';
|
||||||
import MessageContainer from './MessageContainer';
|
import MessageContainer from './MessageContainer';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import { ConversationDetailType, DomainType, MessageItem, MessageTypeEnum } from './type';
|
import {
|
||||||
import { getDomainList, updateConversationName } from './service';
|
ConversationDetailType,
|
||||||
|
DefaultEntityType,
|
||||||
|
DomainType,
|
||||||
|
MessageItem,
|
||||||
|
MessageTypeEnum,
|
||||||
|
} from './type';
|
||||||
|
import { getDomainList } from './service';
|
||||||
import { useThrottleFn } from 'ahooks';
|
import { useThrottleFn } from 'ahooks';
|
||||||
import RightSection from './RightSection';
|
import Conversation from './Conversation';
|
||||||
import ChatFooter from './ChatFooter';
|
import ChatFooter from './ChatFooter';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { CHAT_TITLE, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from './constants';
|
import { CHAT_TITLE, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from './constants';
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-sdk';
|
import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-sdk';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import 'supersonic-chat-sdk/dist/index.css';
|
import 'supersonic-chat-sdk/dist/index.css';
|
||||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||||
import { TOKEN_KEY } from '@/services/request';
|
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
||||||
import Conversation from './Conversation';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isCopilotMode?: boolean;
|
isCopilotMode?: boolean;
|
||||||
|
copilotFullscreen?: boolean;
|
||||||
|
defaultDomainName?: string;
|
||||||
|
defaultEntityFilter?: DefaultEntityType;
|
||||||
|
copilotSendMsg?: string;
|
||||||
|
triggerNewConversation?: boolean;
|
||||||
|
onNewConversationTriggered?: () => void;
|
||||||
|
onCurrentDomainChange?: (domain?: DomainType) => void;
|
||||||
|
onCancelCopilotFilter?: () => void;
|
||||||
|
onCheckMoreDetail?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
const Chat: React.FC<Props> = ({
|
||||||
const isMobileMode = (isMobile || isCopilotMode) as boolean;
|
isCopilotMode,
|
||||||
|
copilotFullscreen,
|
||||||
|
defaultDomainName,
|
||||||
|
defaultEntityFilter,
|
||||||
|
copilotSendMsg,
|
||||||
|
triggerNewConversation,
|
||||||
|
onNewConversationTriggered,
|
||||||
|
onCurrentDomainChange,
|
||||||
|
onCancelCopilotFilter,
|
||||||
|
onCheckMoreDetail,
|
||||||
|
}) => {
|
||||||
|
const isMobileMode = isMobile || isCopilotMode;
|
||||||
|
const localConversationCollapsed = localStorage.getItem('CONVERSATION_COLLAPSED');
|
||||||
|
|
||||||
const [messageList, setMessageList] = useState<MessageItem[]>([]);
|
const [messageList, setMessageList] = useState<MessageItem[]>([]);
|
||||||
const [inputMsg, setInputMsg] = useState('');
|
const [inputMsg, setInputMsg] = useState('');
|
||||||
@@ -32,68 +58,137 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
const [currentConversation, setCurrentConversation] = useState<
|
const [currentConversation, setCurrentConversation] = useState<
|
||||||
ConversationDetailType | undefined
|
ConversationDetailType | undefined
|
||||||
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
|
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
|
||||||
const [currentEntity, setCurrentEntity] = useState<MsgDataType>();
|
const [conversationCollapsed, setConversationCollapsed] = useState(
|
||||||
const [miniProgramLoading, setMiniProgramLoading] = useState(false);
|
!localConversationCollapsed ? true : localConversationCollapsed === 'true',
|
||||||
|
);
|
||||||
const [domains, setDomains] = useState<DomainType[]>([]);
|
const [domains, setDomains] = useState<DomainType[]>([]);
|
||||||
const [currentDomain, setCurrentDomain] = useState<DomainType>();
|
const [currentDomain, setCurrentDomain] = useState<DomainType>();
|
||||||
const [conversationCollapsed, setConversationCollapsed] = useState(false);
|
const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
|
||||||
|
const [applyAuthVisible, setApplyAuthVisible] = useState(false);
|
||||||
|
const [applyAuthDomain, setApplyAuthDomain] = useState('');
|
||||||
|
const [initialDomainName, setInitialDomainName] = useState('');
|
||||||
|
const location = useLocation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { domainName } = (location as any).query;
|
||||||
|
|
||||||
const conversationRef = useRef<any>();
|
const conversationRef = useRef<any>();
|
||||||
const chatFooterRef = useRef<any>();
|
const chatFooterRef = useRef<any>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
|
||||||
|
initDomains();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (domains.length > 0 && initialDomainName && !currentDomain) {
|
||||||
|
changeDomain(domains.find((domain) => domain.name === initialDomainName));
|
||||||
|
}
|
||||||
|
}, [domains]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (domainName) {
|
||||||
|
setInitialDomainName(domainName);
|
||||||
|
}
|
||||||
|
}, [domainName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultDomainName !== undefined && domains.length > 0) {
|
||||||
|
changeDomain(domains.find((domain) => domain.name === defaultDomainName));
|
||||||
|
}
|
||||||
|
}, [defaultDomainName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentConversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { initMsg, domainId, entityId } = currentConversation;
|
||||||
|
if (initMsg) {
|
||||||
|
inputFocus();
|
||||||
|
if (initMsg === 'CUSTOMIZE' && copilotSendMsg) {
|
||||||
|
onSendMsg(copilotSendMsg, [], domainId, entityId);
|
||||||
|
dispatch({
|
||||||
|
type: 'globalState/setCopilotSendMsg',
|
||||||
|
payload: '',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (initMsg === DEFAULT_CONVERSATION_NAME || initMsg.includes('CUSTOMIZE')) {
|
||||||
|
sendHelloRsp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSendMsg(initMsg, [], domainId, entityId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateHistoryMsg(1);
|
||||||
|
setPageNo(1);
|
||||||
|
}, [currentConversation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDefaultEntity(defaultEntityFilter);
|
||||||
|
}, [defaultEntityFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (historyInited) {
|
||||||
|
const messageContainerEle = document.getElementById('messageContainer');
|
||||||
|
messageContainerEle?.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
const messageContainerEle = document.getElementById('messageContainer');
|
||||||
|
messageContainerEle?.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [historyInited]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputFocus();
|
||||||
|
}, [copilotFullscreen]);
|
||||||
|
|
||||||
const sendHelloRsp = () => {
|
const sendHelloRsp = () => {
|
||||||
setMessageList([
|
setMessageList([
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: MessageTypeEnum.RECOMMEND_QUESTIONS,
|
type: MessageTypeEnum.TEXT,
|
||||||
// msg: '您好,请问有什么我能帮您吗?',
|
msg: defaultDomainName
|
||||||
|
? `您好,请输入关于${
|
||||||
|
defaultEntityFilter?.entityName
|
||||||
|
? `${defaultDomainName?.slice(0, defaultDomainName?.length - 1)}【${
|
||||||
|
defaultEntityFilter?.entityName
|
||||||
|
}】`
|
||||||
|
: `【${defaultDomainName}】`
|
||||||
|
}的问题`
|
||||||
|
: '您好,请问有什么我能帮您吗?',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const existInstuctionMsg = (list: HistoryMsgItemType[]) => {
|
const convertHistoryMsg = (list: HistoryMsgItemType[]) => {
|
||||||
return list.some((msg) => msg.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION);
|
return list.map((item: HistoryMsgItemType) => ({
|
||||||
};
|
id: item.questionId,
|
||||||
|
type:
|
||||||
const updateScroll = (list: HistoryMsgItemType[]) => {
|
item.queryResult?.queryMode === MessageTypeEnum.PLUGIN ||
|
||||||
if (existInstuctionMsg(list)) {
|
item.queryResult?.queryMode === MessageTypeEnum.WEB_PAGE
|
||||||
setMiniProgramLoading(true);
|
? MessageTypeEnum.PLUGIN
|
||||||
setTimeout(() => {
|
: MessageTypeEnum.QUESTION,
|
||||||
setMiniProgramLoading(false);
|
msg: item.queryText,
|
||||||
updateMessageContainerScroll();
|
msgData: item.queryResult,
|
||||||
}, 3000);
|
score: item.score,
|
||||||
} else {
|
isHistory: true,
|
||||||
updateMessageContainerScroll();
|
}));
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHistoryMsg = async (page: number) => {
|
const updateHistoryMsg = async (page: number) => {
|
||||||
const res = await getHistoryMsg(page, currentConversation!.chatId, 3);
|
const res = await getHistoryMsg(page, currentConversation!.chatId, 3);
|
||||||
const { hasNextPage, list } = res.data?.data || { hasNextPage: false, list: [] };
|
const { hasNextPage, list } = res.data?.data || { hasNextPage: false, list: [] };
|
||||||
setMessageList([
|
const msgList = [...convertHistoryMsg(list), ...(page === 1 ? [] : messageList)];
|
||||||
...list.map((item: HistoryMsgItemType) => ({
|
setMessageList(msgList);
|
||||||
id: item.questionId,
|
|
||||||
type:
|
|
||||||
item.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION
|
|
||||||
? MessageTypeEnum.INSTRUCTION
|
|
||||||
: MessageTypeEnum.QUESTION,
|
|
||||||
msg: item.queryText,
|
|
||||||
msgData: item.queryResult,
|
|
||||||
isHistory: true,
|
|
||||||
})),
|
|
||||||
...(page === 1 ? [] : messageList),
|
|
||||||
]);
|
|
||||||
setHasNextPage(hasNextPage);
|
setHasNextPage(hasNextPage);
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
sendHelloRsp();
|
sendHelloRsp();
|
||||||
} else {
|
|
||||||
setCurrentEntity(list[list.length - 1].queryResult);
|
|
||||||
}
|
}
|
||||||
updateScroll(list);
|
updateMessageContainerScroll();
|
||||||
setHistoryInited(true);
|
setHistoryInited(true);
|
||||||
inputFocus();
|
inputFocus();
|
||||||
}
|
} else {
|
||||||
if (page > 1) {
|
|
||||||
const msgEle = document.getElementById(`${messageList[0]?.id}`);
|
const msgEle = document.getElementById(`${messageList[0]?.id}`);
|
||||||
msgEle?.scrollIntoView();
|
msgEle?.scrollIntoView();
|
||||||
}
|
}
|
||||||
@@ -113,31 +208,21 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const initDomains = async () => {
|
const changeDomain = (domain?: DomainType) => {
|
||||||
try {
|
setCurrentDomain(domain);
|
||||||
const res = await getDomainList();
|
if (onCurrentDomainChange) {
|
||||||
const domainList = getLeafList(res.data);
|
onCurrentDomainChange(domain);
|
||||||
setDomains(
|
}
|
||||||
[{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...domainList].slice(0, 11),
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const initDomains = async () => {
|
||||||
setChatSdkToken(localStorage.getItem(TOKEN_KEY) || '');
|
const res = await getDomainList();
|
||||||
initDomains();
|
const domainList = getLeafList(res.data);
|
||||||
}, []);
|
setDomains([{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...domainList].slice(0, 11));
|
||||||
|
if (defaultDomainName !== undefined) {
|
||||||
useEffect(() => {
|
changeDomain(domainList.find((domain) => domain.name === defaultDomainName));
|
||||||
if (historyInited) {
|
|
||||||
const messageContainerEle = document.getElementById('messageContainer');
|
|
||||||
messageContainerEle?.addEventListener('scroll', handleScroll);
|
|
||||||
}
|
}
|
||||||
return () => {
|
};
|
||||||
const messageContainerEle = document.getElementById('messageContainer');
|
|
||||||
messageContainerEle?.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
}, [historyInited]);
|
|
||||||
|
|
||||||
const inputFocus = () => {
|
const inputFocus = () => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
@@ -149,34 +234,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
chatFooterRef.current?.inputBlur();
|
chatFooterRef.current?.inputBlur();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const onSendMsg = async (
|
||||||
if (!currentConversation) {
|
msg?: string,
|
||||||
return;
|
list?: MessageItem[],
|
||||||
}
|
domainId?: number,
|
||||||
setCurrentEntity(undefined);
|
entityId?: string,
|
||||||
const { initMsg, domainId } = currentConversation;
|
) => {
|
||||||
if (initMsg) {
|
|
||||||
inputFocus();
|
|
||||||
if (initMsg === DEFAULT_CONVERSATION_NAME) {
|
|
||||||
sendHelloRsp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSendMsg(currentConversation.initMsg, [], domainId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateHistoryMsg(1);
|
|
||||||
setPageNo(1);
|
|
||||||
}, [currentConversation]);
|
|
||||||
|
|
||||||
const modifyConversationName = async (name: string) => {
|
|
||||||
await updateConversationName(name, currentConversation!.chatId);
|
|
||||||
if (!isMobileMode) {
|
|
||||||
conversationRef?.current?.updateData();
|
|
||||||
window.history.replaceState('', '', `?q=${name}&cid=${currentConversation!.chatId}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSendMsg = async (msg?: string, list?: MessageItem[], domainId?: number) => {
|
|
||||||
const currentMsg = msg || inputMsg;
|
const currentMsg = msg || inputMsg;
|
||||||
if (currentMsg.trim() === '') {
|
if (currentMsg.trim() === '') {
|
||||||
setInputMsg('');
|
setInputMsg('');
|
||||||
@@ -184,8 +247,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
}
|
}
|
||||||
const msgDomain = domains.find((item) => currentMsg.includes(item.name));
|
const msgDomain = domains.find((item) => currentMsg.includes(item.name));
|
||||||
const certainDomain = currentMsg[0] === '@' && msgDomain;
|
const certainDomain = currentMsg[0] === '@' && msgDomain;
|
||||||
|
let domainChanged = false;
|
||||||
|
|
||||||
if (certainDomain) {
|
if (certainDomain) {
|
||||||
setCurrentDomain(msgDomain.id === -1 ? undefined : msgDomain);
|
const toDomain = msgDomain.id === -1 ? undefined : msgDomain;
|
||||||
|
changeDomain(toDomain);
|
||||||
|
domainChanged = currentDomain?.id !== toDomain?.id;
|
||||||
}
|
}
|
||||||
const domainIdValue = domainId || msgDomain?.id || currentDomain?.id;
|
const domainIdValue = domainId || msgDomain?.id || currentDomain?.id;
|
||||||
const msgs = [
|
const msgs = [
|
||||||
@@ -195,6 +262,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
msg: currentMsg,
|
msg: currentMsg,
|
||||||
msgValue: certainDomain ? currentMsg.replace(`@${msgDomain.name}`, '').trim() : currentMsg,
|
msgValue: certainDomain ? currentMsg.replace(`@${msgDomain.name}`, '').trim() : currentMsg,
|
||||||
domainId: domainIdValue === -1 ? undefined : domainIdValue,
|
domainId: domainIdValue === -1 ? undefined : domainIdValue,
|
||||||
|
entityId: entityId || (domainChanged ? undefined : defaultEntity?.entityId),
|
||||||
identityMsg: certainDomain ? getIdentityMsgText(msgDomain) : undefined,
|
identityMsg: certainDomain ? getIdentityMsgText(msgDomain) : undefined,
|
||||||
type: MessageTypeEnum.QUESTION,
|
type: MessageTypeEnum.QUESTION,
|
||||||
},
|
},
|
||||||
@@ -202,11 +270,6 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
setMessageList(msgs);
|
setMessageList(msgs);
|
||||||
updateMessageContainerScroll();
|
updateMessageContainerScroll();
|
||||||
setInputMsg('');
|
setInputMsg('');
|
||||||
modifyConversationName(currentMsg);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleCollapseBtn = () => {
|
|
||||||
setConversationCollapsed(!conversationCollapsed);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInputMsgChange = (value: string) => {
|
const onInputMsgChange = (value: string) => {
|
||||||
@@ -224,29 +287,38 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectConversation = (conversation: ConversationDetailType, name?: string) => {
|
const onSelectConversation = (
|
||||||
|
conversation: ConversationDetailType,
|
||||||
|
name?: string,
|
||||||
|
domainId?: number,
|
||||||
|
entityId?: string,
|
||||||
|
) => {
|
||||||
if (!isMobileMode) {
|
if (!isMobileMode) {
|
||||||
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
|
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
|
||||||
}
|
}
|
||||||
setCurrentConversation({
|
setCurrentConversation({
|
||||||
...conversation,
|
...conversation,
|
||||||
initMsg: name,
|
initMsg: name,
|
||||||
|
domainId,
|
||||||
|
entityId,
|
||||||
});
|
});
|
||||||
saveConversationToLocal(conversation);
|
saveConversationToLocal(conversation);
|
||||||
setCurrentDomain(undefined);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => {
|
const onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => {
|
||||||
|
if (!isMobile) {
|
||||||
|
conversationRef?.current?.updateData();
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.queryMode === 'INSTRUCTION') {
|
if (data.queryMode === 'WEB_PAGE') {
|
||||||
setMessageList([
|
setMessageList([
|
||||||
...messageList.slice(0, messageList.length - 1),
|
...messageList,
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
msg: data.response.name || messageList[messageList.length - 1]?.msg,
|
msg: messageList[messageList.length - 1]?.msg,
|
||||||
type: MessageTypeEnum.INSTRUCTION,
|
type: MessageTypeEnum.PLUGIN,
|
||||||
msgData: data,
|
msgData: data,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -259,7 +331,6 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
}
|
}
|
||||||
updateMessageContainerScroll();
|
updateMessageContainerScroll();
|
||||||
}
|
}
|
||||||
setCurrentEntity(data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCheckMore = (data: MsgDataType) => {
|
const onCheckMore = (data: MsgDataType) => {
|
||||||
@@ -268,11 +339,19 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
msg: data.response.name,
|
msg: data.response.name,
|
||||||
type: MessageTypeEnum.INSTRUCTION,
|
type: MessageTypeEnum.PLUGIN,
|
||||||
msgData: data,
|
msgData: data,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
updateMessageContainerScroll();
|
updateMessageContainerScroll();
|
||||||
|
if (onCheckMoreDetail) {
|
||||||
|
onCheckMoreDetail();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleCollapseBtn = () => {
|
||||||
|
setConversationCollapsed(!conversationCollapsed);
|
||||||
|
localStorage.setItem('CONVERSATION_COLLAPSED', `${!conversationCollapsed}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIdentityMsgText = (domain?: DomainType) => {
|
const getIdentityMsgText = (domain?: DomainType) => {
|
||||||
@@ -281,26 +360,19 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
: '您好,我将尽力帮您解答所有主题相关问题~';
|
: '您好,我将尽力帮您解答所有主题相关问题~';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIdentityMsg = (domain?: DomainType) => {
|
const onApplyAuth = (domain: string) => {
|
||||||
return {
|
setApplyAuthDomain(domain);
|
||||||
id: uuid(),
|
setApplyAuthVisible(true);
|
||||||
type: MessageTypeEnum.TEXT,
|
|
||||||
msg: getIdentityMsgText(domain),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectDomain = (domain: DomainType) => {
|
const onAddConversation = () => {
|
||||||
const domainValue = currentDomain?.id === domain.id ? undefined : domain;
|
conversationRef.current?.onAddConversation();
|
||||||
setCurrentDomain(domainValue);
|
|
||||||
setCurrentEntity(undefined);
|
|
||||||
setMessageList([...messageList, getIdentityMsg(domainValue)]);
|
|
||||||
updateMessageContainerScroll();
|
|
||||||
inputFocus();
|
inputFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatClass = classNames(styles.chat, {
|
const chatClass = classNames(styles.chat, {
|
||||||
[styles.mobile]: isMobileMode,
|
[styles.mobile]: isMobileMode,
|
||||||
[styles.copilot]: isCopilotMode,
|
[styles.copilotFullscreen]: copilotFullscreen,
|
||||||
[styles.conversationCollapsed]: conversationCollapsed,
|
[styles.conversationCollapsed]: conversationCollapsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -312,6 +384,11 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
<Conversation
|
<Conversation
|
||||||
currentConversation={currentConversation}
|
currentConversation={currentConversation}
|
||||||
collapsed={conversationCollapsed}
|
collapsed={conversationCollapsed}
|
||||||
|
isCopilotMode={isCopilotMode}
|
||||||
|
defaultDomainName={defaultDomainName}
|
||||||
|
defaultEntityFilter={defaultEntityFilter}
|
||||||
|
triggerNewConversation={triggerNewConversation}
|
||||||
|
onNewConversationTriggered={onNewConversationTriggered}
|
||||||
onSelectConversation={onSelectConversation}
|
onSelectConversation={onSelectConversation}
|
||||||
ref={conversationRef}
|
ref={conversationRef}
|
||||||
/>
|
/>
|
||||||
@@ -325,20 +402,21 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
chatId={currentConversation?.chatId}
|
chatId={currentConversation?.chatId}
|
||||||
isMobileMode={isMobileMode}
|
isMobileMode={isMobileMode}
|
||||||
conversationCollapsed={conversationCollapsed}
|
conversationCollapsed={conversationCollapsed}
|
||||||
onClickMessageContainer={() => {
|
copilotFullscreen={copilotFullscreen}
|
||||||
inputFocus();
|
onClickMessageContainer={inputFocus}
|
||||||
}}
|
|
||||||
onMsgDataLoaded={onMsgDataLoaded}
|
onMsgDataLoaded={onMsgDataLoaded}
|
||||||
onSelectSuggestion={onSendMsg}
|
|
||||||
onCheckMore={onCheckMore}
|
onCheckMore={onCheckMore}
|
||||||
|
onApplyAuth={onApplyAuth}
|
||||||
/>
|
/>
|
||||||
<ChatFooter
|
<ChatFooter
|
||||||
inputMsg={inputMsg}
|
inputMsg={inputMsg}
|
||||||
chatId={currentConversation?.chatId}
|
chatId={currentConversation?.chatId}
|
||||||
domains={domains}
|
domains={domains}
|
||||||
currentDomain={currentDomain}
|
currentDomain={currentDomain}
|
||||||
|
defaultEntity={defaultEntity}
|
||||||
collapsed={conversationCollapsed}
|
collapsed={conversationCollapsed}
|
||||||
isMobileMode={isMobileMode}
|
isCopilotMode={isCopilotMode}
|
||||||
|
copilotFullscreen={copilotFullscreen}
|
||||||
onToggleCollapseBtn={onToggleCollapseBtn}
|
onToggleCollapseBtn={onToggleCollapseBtn}
|
||||||
onInputMsgChange={onInputMsgChange}
|
onInputMsgChange={onInputMsgChange}
|
||||||
onSendMsg={(msg: string, domainId?: number) => {
|
onSendMsg={(msg: string, domainId?: number) => {
|
||||||
@@ -347,9 +425,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
inputBlur();
|
inputBlur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onAddConversation={() => {
|
onAddConversation={onAddConversation}
|
||||||
conversationRef.current?.onAddConversation();
|
onCancelDefaultFilter={() => {
|
||||||
inputFocus();
|
changeDomain(undefined);
|
||||||
|
if (onCancelCopilotFilter) {
|
||||||
|
onCancelCopilotFilter();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
ref={chatFooterRef}
|
ref={chatFooterRef}
|
||||||
/>
|
/>
|
||||||
@@ -357,17 +438,6 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* {!isMobileMode && (
|
|
||||||
<RightSection
|
|
||||||
domains={domains}
|
|
||||||
currentEntity={currentEntity}
|
|
||||||
currentDomain={currentDomain}
|
|
||||||
currentConversation={currentConversation}
|
|
||||||
onSelectDomain={onSelectDomain}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
conversationRef={conversationRef}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,10 +24,19 @@ export function getAllConversations() {
|
|||||||
return request<Result<any>>(`${prefix}/chat/manage/getAll`);
|
return request<Result<any>>(`${prefix}/chat/manage/getAll`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMiniProgramList(entityId: string, domainId: number) {
|
||||||
|
return request<Result<any>>(
|
||||||
|
`${prefix}/chat/plugin/extend/getAvailablePlugin/${entityId}/${domainId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
skipErrorHandler: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getDomainList() {
|
export function getDomainList() {
|
||||||
return request<Result<DomainType[]>>(`${prefix}/chat/conf/domainList/view`, {
|
return request<Result<DomainType[]>>(`${prefix}/chat/conf/domainList/view`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
skipErrorHandler: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +49,12 @@ export function updateQAFeedback(questionId: number, score: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function queryMetricSuggestion(domainId: number) {
|
||||||
|
return request<Result<any>>(`${prefix}/chat/recommend/metric/${domainId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function querySuggestion(domainId: number) {
|
export function querySuggestion(domainId: number) {
|
||||||
return request<Result<any>>(`${prefix}/chat/recommend/${domainId}`, {
|
return request<Result<any>>(`${prefix}/chat/recommend/${domainId}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: calc(100vw - 260px);
|
width: calc(100vw - 260px);
|
||||||
height: calc(100vh - 48px);
|
height: calc(100vh - 48px) !important;
|
||||||
padding-left: 10px;
|
padding-left: 6px;
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
|
||||||
.emptyHolder {
|
.emptyHolder {
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
.messageList {
|
.messageList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px 20px 90px 4px;
|
padding: 20px 20px 60px 4px;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
|
|
||||||
.messageItem {
|
.messageItem {
|
||||||
@@ -109,6 +109,16 @@
|
|||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
|
.ant-table-small {
|
||||||
|
.ant-table-tbody {
|
||||||
|
.ant-table-cell {
|
||||||
|
padding: 6px 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ss-chat-table-formatted-value {
|
||||||
|
font-size: 15px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
.ant-table-row {
|
.ant-table-row {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
@@ -218,12 +228,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.miniProgramLoading {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10000px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,7 +236,7 @@
|
|||||||
|
|
||||||
&.conversationCollapsed {
|
&.conversationCollapsed {
|
||||||
.chatApp {
|
.chatApp {
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,19 +250,193 @@
|
|||||||
|
|
||||||
.conversation {
|
.conversation {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
|
||||||
|
.conversationList {
|
||||||
|
height: calc(100% - 50px) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatApp {
|
.chatApp {
|
||||||
width: 100% !important;
|
width: calc(100% - 260px) !important;
|
||||||
|
width: 100%;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copilotFullscreen {
|
||||||
|
.chatApp {
|
||||||
|
width: calc(100% - 260px) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.conversationCollapsed {
|
||||||
|
.chatApp {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation {
|
||||||
|
position: relative;
|
||||||
|
width: 260px;
|
||||||
|
height: calc(100vh - 48px) !important;
|
||||||
|
background-color: #fff;
|
||||||
|
border-right: 1px solid var(--border-color-base);
|
||||||
|
|
||||||
|
.leftSection {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.searchConversation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 9px 10px;
|
||||||
|
|
||||||
|
.searchIcon {
|
||||||
|
color: #999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchTask {
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-input {
|
||||||
|
font-size: 13px !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationList {
|
||||||
|
height: calc(calc(100vh - 48px) - 50px);
|
||||||
|
padding: 2px 8px 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
.conversationItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.conversationIcon {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationContent {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.topTitleBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.conversationName {
|
||||||
|
width: 150px;
|
||||||
|
margin-right: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationTime {
|
||||||
|
color: var(--text-color-six);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-color-six);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activeConversationItem {
|
||||||
|
background-color: var(--light-blue-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--light-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateSection {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.operateIcon {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: var(--text-color-fourth);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateLabel {
|
||||||
|
color: var(--text-color-third);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.operateLabel {
|
||||||
|
color: var(--chat-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-right: 0;
|
||||||
|
|
||||||
|
.leftSection {
|
||||||
|
.searchConversation {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationList {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operateSection {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copilotMode {
|
||||||
|
&.collapsed {
|
||||||
|
width: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
.messageList {
|
.messageList {
|
||||||
padding: 20px 12px 20px !important;
|
padding: 20px 12px 60px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +504,6 @@
|
|||||||
|
|
||||||
.example {
|
.example {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
// margin-left: 16px;
|
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -409,7 +586,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
// width: 530px;
|
|
||||||
|
|
||||||
&.recentSearchBar {
|
&.recentSearchBar {
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
@@ -607,4 +783,14 @@
|
|||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ss-chat-item-typing-bubble {
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ss-chat-metric-card-drill-down-dimensions {
|
||||||
|
bottom: -38px !important;
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ 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', // 插件
|
PLUGIN = 'PLUGIN', // 插件
|
||||||
SUGGESTION = 'SUGGESTION',
|
WEB_PAGE = 'WEB_PAGE', // 插件
|
||||||
RECOMMEND_QUESTIONS = 'RECOMMEND_QUESTIONS' // 推荐问题
|
RECOMMEND_QUESTIONS = 'recommend_questions', // 推荐问题
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageItem = {
|
export type MessageItem = {
|
||||||
@@ -17,8 +19,11 @@ export type MessageItem = {
|
|||||||
msgValue?: string;
|
msgValue?: string;
|
||||||
identityMsg?: string;
|
identityMsg?: string;
|
||||||
domainId?: number;
|
domainId?: number;
|
||||||
|
entityId?: string;
|
||||||
msgData?: MsgDataType;
|
msgData?: MsgDataType;
|
||||||
quote?: string;
|
quote?: string;
|
||||||
|
score?: number;
|
||||||
|
feedback?: string;
|
||||||
isHistory?: boolean;
|
isHistory?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +36,7 @@ export type ConversationDetailType = {
|
|||||||
lastTime?: string;
|
lastTime?: string;
|
||||||
initMsg?: string;
|
initMsg?: string;
|
||||||
domainId?: number;
|
domainId?: number;
|
||||||
|
entityId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum MessageModeEnum {
|
export enum MessageModeEnum {
|
||||||
@@ -44,6 +50,25 @@ export type DomainType = {
|
|||||||
bizName: string;
|
bizName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum PluginShowTypeEnum {
|
||||||
|
DASHBOARD = 'DASHBOARD',
|
||||||
|
WIDGET = 'WIDGET',
|
||||||
|
URL = 'URL',
|
||||||
|
TAG = 'TAG',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginType = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DefaultEntityType = {
|
||||||
|
entityId: string;
|
||||||
|
entityName: string;
|
||||||
|
domainName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SuggestionItemType = {
|
export type SuggestionItemType = {
|
||||||
id: number;
|
id: number;
|
||||||
domain: number;
|
domain: number;
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, Select, Form, Input, InputNumber, message, Button, Radio } from 'antd';
|
||||||
|
import { getDimensionList, getDomainList, savePlugin } from './service';
|
||||||
|
import {
|
||||||
|
DimensionType,
|
||||||
|
DomainType,
|
||||||
|
ParamTypeEnum,
|
||||||
|
ParseModeEnum,
|
||||||
|
PluginType,
|
||||||
|
FunctionParamFormItemType,
|
||||||
|
PluginTypeEnum,
|
||||||
|
} from './type';
|
||||||
|
import { getLeafList, uuid } from '@/utils/utils';
|
||||||
|
import styles from './style.less';
|
||||||
|
import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants';
|
||||||
|
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { isArray, set } from 'lodash';
|
||||||
|
|
||||||
|
const FormItem = Form.Item;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
detail?: PluginType;
|
||||||
|
onSubmit: (values: any) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
|
||||||
|
const [domainList, setDomainList] = useState<DomainType[]>([]);
|
||||||
|
const [domainDimensionList, setDomainDimensionList] = useState<Record<number, DimensionType[]>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const [pluginType, setPluginType] = useState<PluginTypeEnum>();
|
||||||
|
const [functionName, setFunctionName] = useState<string>();
|
||||||
|
const [functionParams, setFunctionParams] = useState<FunctionParamFormItemType[]>([]);
|
||||||
|
const [examples, setExamples] = useState<{ id: string; question?: string }[]>([]);
|
||||||
|
const [filters, setFilters] = useState<any[]>([]);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const initDomainList = async () => {
|
||||||
|
const res = await getDomainList();
|
||||||
|
setDomainList([{ id: -1, name: '全部' }, ...getLeafList(res.data)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initDomainList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initDomainDimensions = async (params: any) => {
|
||||||
|
const domainIds = params
|
||||||
|
.filter((param: any) => !!param.domainId)
|
||||||
|
.map((param: any) => param.domainId);
|
||||||
|
const res = await Promise.all(domainIds.map((domainId: number) => getDimensionList(domainId)));
|
||||||
|
setDomainDimensionList(
|
||||||
|
domainIds.reduce(
|
||||||
|
(result: Record<number, DimensionType[]>, domainId: number, index: number) => {
|
||||||
|
result[domainId] = res[index].data.list;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (detail) {
|
||||||
|
const { paramOptions } = detail.config || {};
|
||||||
|
const height = paramOptions?.find(
|
||||||
|
(option: any) => option.paramType === 'FORWARD' && option.key === 'height',
|
||||||
|
)?.value;
|
||||||
|
form.setFieldsValue({
|
||||||
|
...detail,
|
||||||
|
url: detail.config?.url,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
if (paramOptions?.length > 0) {
|
||||||
|
const params = paramOptions.filter(
|
||||||
|
(option: any) => option.paramType !== ParamTypeEnum.FORWARD,
|
||||||
|
);
|
||||||
|
setFilters(params);
|
||||||
|
initDomainDimensions(params);
|
||||||
|
}
|
||||||
|
setPluginType(detail.type);
|
||||||
|
const parseModeObj = JSON.parse(detail.parseModeConfig || '{}');
|
||||||
|
setFunctionName(parseModeObj.name);
|
||||||
|
const { properties } = parseModeObj.parameters || {};
|
||||||
|
setFunctionParams(
|
||||||
|
properties
|
||||||
|
? Object.keys(properties).map((key: string, index: number) => {
|
||||||
|
return {
|
||||||
|
id: `${index}`,
|
||||||
|
name: key,
|
||||||
|
type: properties[key].type,
|
||||||
|
description: properties[key].description,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
setExamples(
|
||||||
|
parseModeObj.examples
|
||||||
|
? parseModeObj.examples.map((item: string, index: number) => ({
|
||||||
|
id: index,
|
||||||
|
question: item,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [detail]);
|
||||||
|
|
||||||
|
const layout = {
|
||||||
|
labelCol: { span: 4 },
|
||||||
|
wrapperCol: { span: 20 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFunctionParam = (description: string) => {
|
||||||
|
return {
|
||||||
|
name: functionName,
|
||||||
|
description,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: functionParams
|
||||||
|
.filter((param) => !!param.name?.trim())
|
||||||
|
.reduce((acc, cur) => {
|
||||||
|
acc[cur.name || ''] = {
|
||||||
|
type: cur.type,
|
||||||
|
description: cur.description,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
required: functionParams.filter((param) => !!param.name?.trim()).map((param) => param.name),
|
||||||
|
},
|
||||||
|
examples: examples
|
||||||
|
.filter((example) => !!example.question?.trim())
|
||||||
|
.map((example) => example.question),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setConfirmLoading(true);
|
||||||
|
let paramOptions = isArray(filters)
|
||||||
|
? filters?.filter(
|
||||||
|
(filter) =>
|
||||||
|
typeof filter === 'object' && (filter.paramType !== null || filter.value != null),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
paramOptions = paramOptions.concat([
|
||||||
|
{
|
||||||
|
paramType: ParamTypeEnum.FORWARD,
|
||||||
|
key: 'height',
|
||||||
|
value: values.height || undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const config = {
|
||||||
|
url: values.url,
|
||||||
|
paramOptions,
|
||||||
|
};
|
||||||
|
await savePlugin({
|
||||||
|
...values,
|
||||||
|
id: detail?.id,
|
||||||
|
domainList: isArray(values.domainList) ? values.domainList : [values.domainList],
|
||||||
|
config: JSON.stringify(config),
|
||||||
|
parseModeConfig: JSON.stringify(getFunctionParam(values.pattern)),
|
||||||
|
});
|
||||||
|
setConfirmLoading(false);
|
||||||
|
onSubmit(values);
|
||||||
|
message.success(detail?.id ? '编辑成功' : '新建成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDimensionList = async (value: number) => {
|
||||||
|
if (domainDimensionList[value]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await getDimensionList(value);
|
||||||
|
setDomainDimensionList({ ...domainDimensionList, [value]: res.data.list });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open
|
||||||
|
title={detail ? '编辑插件' : '新建插件'}
|
||||||
|
width={900}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<Form {...layout} form={form} style={{ maxWidth: 820 }}>
|
||||||
|
<FormItem name="domainList" label="主题域">
|
||||||
|
<Select
|
||||||
|
placeholder="请选择主题域"
|
||||||
|
options={domainList.map((domain) => ({
|
||||||
|
label: domain.name,
|
||||||
|
value: domain.id,
|
||||||
|
}))}
|
||||||
|
showSearch
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
|
||||||
|
}
|
||||||
|
mode="multiple"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
name="name"
|
||||||
|
label="插件名称"
|
||||||
|
rules={[{ required: true, message: '请输入插件名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入插件名称" allowClear />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
name="type"
|
||||||
|
label="插件类型"
|
||||||
|
rules={[{ required: true, message: '请选择插件类型' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择插件类型"
|
||||||
|
options={Object.keys(PLUGIN_TYPE_MAP).map((key) => ({
|
||||||
|
label: PLUGIN_TYPE_MAP[key],
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
onChange={(value) => {
|
||||||
|
setPluginType(value);
|
||||||
|
if (value === PluginTypeEnum.DSL) {
|
||||||
|
form.setFieldsValue({ parseMode: ParseModeEnum.FUNCTION_CALL });
|
||||||
|
// setFunctionName('DSL');
|
||||||
|
setFunctionParams([
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: 'query_text',
|
||||||
|
type: 'string',
|
||||||
|
description: '用户的原始自然语言查询',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
name="pattern"
|
||||||
|
label="插件描述"
|
||||||
|
rules={[{ required: true, message: '请输入插件描述' }]}
|
||||||
|
>
|
||||||
|
<TextArea placeholder="请输入插件描述,多个描述换行分隔" allowClear />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem name="exampleQuestions" label="示例问题">
|
||||||
|
<div className={styles.paramsSection}>
|
||||||
|
{examples.map((example) => {
|
||||||
|
const { id, question } = example;
|
||||||
|
return (
|
||||||
|
<div className={styles.filterRow} key={id}>
|
||||||
|
<Input
|
||||||
|
placeholder="示例问题"
|
||||||
|
value={question}
|
||||||
|
className={styles.questionExample}
|
||||||
|
onChange={(e) => {
|
||||||
|
example.question = e.target.value;
|
||||||
|
setExamples([...examples]);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<DeleteOutlined
|
||||||
|
onClick={() => {
|
||||||
|
setExamples(examples.filter((item) => item.id !== id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setExamples([...examples, { id: uuid() }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
新增示例问题
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="函数名称">
|
||||||
|
<Input
|
||||||
|
value={functionName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFunctionName(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="请输入函数名称,只能包含因为字母和下划线"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem name="params" label="函数参数" hidden={pluginType === PluginTypeEnum.DSL}>
|
||||||
|
<div className={styles.paramsSection}>
|
||||||
|
{functionParams.map((functionParam: FunctionParamFormItemType) => {
|
||||||
|
const { id, name, type, description } = functionParam;
|
||||||
|
return (
|
||||||
|
<div className={styles.filterRow} key={id}>
|
||||||
|
<Input
|
||||||
|
placeholder="参数名称"
|
||||||
|
value={name}
|
||||||
|
className={styles.filterParamName}
|
||||||
|
onChange={(e) => {
|
||||||
|
functionParam.name = e.target.value;
|
||||||
|
setFunctionParams([...functionParams]);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="参数类型"
|
||||||
|
options={[
|
||||||
|
{ label: '字符串', value: 'string' },
|
||||||
|
{ label: '整型', value: 'int' },
|
||||||
|
]}
|
||||||
|
className={styles.filterParamValueField}
|
||||||
|
allowClear
|
||||||
|
value={type}
|
||||||
|
onChange={(value) => {
|
||||||
|
functionParam.type = value;
|
||||||
|
setFunctionParams([...functionParams]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="参数描述"
|
||||||
|
value={description}
|
||||||
|
className={styles.filterParamValueField}
|
||||||
|
onChange={(e) => {
|
||||||
|
functionParam.description = e.target.value;
|
||||||
|
setFunctionParams([...functionParams]);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<DeleteOutlined
|
||||||
|
onClick={() => {
|
||||||
|
setFunctionParams(functionParams.filter((item) => item.id !== id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFunctionParams([...functionParams, { id: uuid() }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
新增函数参数
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
{(pluginType === PluginTypeEnum.WEB_PAGE || pluginType === PluginTypeEnum.WEB_SERVICE) && (
|
||||||
|
<>
|
||||||
|
<FormItem name="url" label="地址" rules={[{ required: true, message: '请输入地址' }]}>
|
||||||
|
<Input placeholder="请输入地址" allowClear />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem name="params" label="参数">
|
||||||
|
<div className={styles.paramsSection}>
|
||||||
|
{filters.map((filter: any) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.filterRow} key={filter.id}>
|
||||||
|
<Input
|
||||||
|
placeholder="参数名称"
|
||||||
|
value={filter.key}
|
||||||
|
className={styles.filterParamName}
|
||||||
|
onChange={(e) => {
|
||||||
|
filter.key = e.target.value;
|
||||||
|
setFilters([...filters]);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<Radio.Group
|
||||||
|
onChange={(e) => {
|
||||||
|
filter.paramType = e.target.value;
|
||||||
|
setFilters([...filters]);
|
||||||
|
}}
|
||||||
|
value={filter.paramType}
|
||||||
|
>
|
||||||
|
<Radio value={ParamTypeEnum.SEMANTIC}>维度</Radio>
|
||||||
|
<Radio value={ParamTypeEnum.CUSTOM}>自定义</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
{filter.paramType === ParamTypeEnum.CUSTOM && (
|
||||||
|
<Input
|
||||||
|
placeholder="请输入"
|
||||||
|
value={filter.value}
|
||||||
|
className={styles.filterParamValueField}
|
||||||
|
onChange={(e) => {
|
||||||
|
filter.value = e.target.value;
|
||||||
|
setFilters([...filters]);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filter.paramType === ParamTypeEnum.SEMANTIC && (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
placeholder="主题域"
|
||||||
|
options={domainList.map((domain) => ({
|
||||||
|
label: domain.name,
|
||||||
|
value: domain.id,
|
||||||
|
}))}
|
||||||
|
showSearch
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
((option?.label ?? '') as string)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase())
|
||||||
|
}
|
||||||
|
className={styles.filterParamName}
|
||||||
|
allowClear
|
||||||
|
value={filter.domainId}
|
||||||
|
onChange={(value) => {
|
||||||
|
filter.domainId = value;
|
||||||
|
setFilters([...filters]);
|
||||||
|
updateDimensionList(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择维度,需先选择主题域"
|
||||||
|
options={(domainDimensionList[filter.domainId] || []).map(
|
||||||
|
(dimension) => ({
|
||||||
|
label: dimension.name,
|
||||||
|
value: `${dimension.id}`,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
showSearch
|
||||||
|
className={styles.filterParamValueField}
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
((option?.label ?? '') as string)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase())
|
||||||
|
}
|
||||||
|
allowClear
|
||||||
|
value={filter.elementId}
|
||||||
|
onChange={(value) => {
|
||||||
|
filter.elementId = value;
|
||||||
|
setFilters([...filters]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<DeleteOutlined
|
||||||
|
onClick={() => {
|
||||||
|
setFilters(filters.filter((item) => item.id !== filter.id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFilters([...filters, { id: uuid(), key: undefined, value: undefined }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
新增参数
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<FormItem name="height" label="高度">
|
||||||
|
<InputNumber placeholder="单位px" />
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailModal;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const PLUGIN_TYPE_MAP = {
|
||||||
|
WEB_PAGE: '外链页面',
|
||||||
|
WEB_SERVICE: 'Web服务',
|
||||||
|
DSL: 'LLM语义解析',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PARSE_MODE_MAP = {
|
||||||
|
EMBEDDING_RECALL: '向量召回',
|
||||||
|
FUNCTION_CALL: '函数调用'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PLUGIN_COLOR_MAP = {
|
||||||
|
WIDGET: 'blue',
|
||||||
|
DASHBOARD: 'volcano',
|
||||||
|
URL: 'purple',
|
||||||
|
TAG: 'cyan',
|
||||||
|
}
|
||||||
248
webapp/packages/supersonic-fe/src/pages/ChatPlugin/index.tsx
Normal file
248
webapp/packages/supersonic-fe/src/pages/ChatPlugin/index.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { getLeafList } from '@/utils/utils';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Input, message, Popconfirm, Select, Table, Tag } from 'antd';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants';
|
||||||
|
import DetailModal from './DetailModal';
|
||||||
|
import { deletePlugin, getDomainList, getPluginList } from './service';
|
||||||
|
import styles from './style.less';
|
||||||
|
import { DomainType, ParseModeEnum, PluginType, PluginTypeEnum } from './type';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
const PluginManage = () => {
|
||||||
|
const [name, setName] = useState<string>();
|
||||||
|
const [type, setType] = useState<PluginTypeEnum>();
|
||||||
|
const [pattern, setPattern] = useState<string>();
|
||||||
|
const [domain, setDomain] = useState<string>();
|
||||||
|
const [data, setData] = useState<PluginType[]>([]);
|
||||||
|
const [domainList, setDomainList] = useState<DomainType[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginType>();
|
||||||
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const initDomainList = async () => {
|
||||||
|
const res = await getDomainList();
|
||||||
|
setDomainList(getLeafList(res.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateData = async (filters?: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await getPluginList({ name, type, pattern, domain, ...(filters || {}) });
|
||||||
|
setLoading(false);
|
||||||
|
setData(res.data.map((item) => ({ ...item, config: JSON.parse(item.config || '{}') })));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initDomainList();
|
||||||
|
updateData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCheckPluginDetail = (record: PluginType) => {
|
||||||
|
setCurrentPluginDetail(record);
|
||||||
|
setDetailModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeletePlugin = async (record: PluginType) => {
|
||||||
|
await deletePlugin(record.id);
|
||||||
|
message.success('插件删除成功');
|
||||||
|
updateData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: any[] = [
|
||||||
|
{
|
||||||
|
title: '插件名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '主题域',
|
||||||
|
dataIndex: 'domainList',
|
||||||
|
key: 'domainList',
|
||||||
|
width: 200,
|
||||||
|
render: (value: number[]) => {
|
||||||
|
if (value?.includes(-1)) {
|
||||||
|
return '全部';
|
||||||
|
}
|
||||||
|
return value ? (
|
||||||
|
<div className={styles.domainColumn}>
|
||||||
|
{value.map((id, index) => {
|
||||||
|
const name = domainList.find((domain) => domain.id === +id)?.name;
|
||||||
|
return name ? <Tag key={id}>{name}</Tag> : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '插件类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
render: (value: string) => {
|
||||||
|
return (
|
||||||
|
<Tag color={value === PluginTypeEnum.WEB_PAGE ? 'blue' : 'cyan'}>
|
||||||
|
{PLUGIN_TYPE_MAP[value]}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '插件描述',
|
||||||
|
dataIndex: 'pattern',
|
||||||
|
key: 'pattern',
|
||||||
|
width: 450,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新人',
|
||||||
|
dataIndex: 'updatedBy',
|
||||||
|
key: 'updatedBy',
|
||||||
|
render: (value: string) => {
|
||||||
|
return value || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
key: 'updatedAt',
|
||||||
|
render: (value: string) => {
|
||||||
|
return value ? moment(value).format('YYYY-MM-DD HH:mm') : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'x',
|
||||||
|
key: 'x',
|
||||||
|
render: (_: any, record: any) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.operator}>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
onCheckPluginDetail(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定删除吗?"
|
||||||
|
onConfirm={() => {
|
||||||
|
onDeletePlugin(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a>删除</a>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onDomainChange = (value: string) => {
|
||||||
|
setDomain(value);
|
||||||
|
updateData({ domain: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypeChange = (value: PluginTypeEnum) => {
|
||||||
|
setType(value);
|
||||||
|
updateData({ type: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
updateData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreatePlugin = () => {
|
||||||
|
setCurrentPluginDetail(undefined);
|
||||||
|
setDetailModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSavePlugin = () => {
|
||||||
|
setDetailModalVisible(false);
|
||||||
|
updateData();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.pluginManage}>
|
||||||
|
<div className={styles.filterSection}>
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterItemTitle}>主题域</div>
|
||||||
|
<Select
|
||||||
|
className={styles.filterItemControl}
|
||||||
|
placeholder="请选择主题域"
|
||||||
|
options={domainList.map((domain) => ({ label: domain.name, value: domain.id }))}
|
||||||
|
value={domain}
|
||||||
|
allowClear
|
||||||
|
onChange={onDomainChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterItemTitle}>插件名称</div>
|
||||||
|
<Search
|
||||||
|
className={styles.filterItemControl}
|
||||||
|
placeholder="请输入插件名称"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setName(e.target.value);
|
||||||
|
}}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterItemTitle}>插件描述</div>
|
||||||
|
<Search
|
||||||
|
className={styles.filterItemControl}
|
||||||
|
placeholder="请输入插件描述"
|
||||||
|
value={pattern}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPattern(e.target.value);
|
||||||
|
}}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterItemTitle}>插件类型</div>
|
||||||
|
<Select
|
||||||
|
className={styles.filterItemControl}
|
||||||
|
placeholder="请选择插件类型"
|
||||||
|
options={Object.keys(PLUGIN_TYPE_MAP).map((key) => ({
|
||||||
|
label: PLUGIN_TYPE_MAP[key],
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
value={type}
|
||||||
|
allowClear
|
||||||
|
onChange={onTypeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pluginList}>
|
||||||
|
<div className={styles.titleBar}>
|
||||||
|
<div className={styles.title}>插件列表</div>
|
||||||
|
<Button type="primary" onClick={onCreatePlugin}>
|
||||||
|
<PlusOutlined />
|
||||||
|
新建插件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
size="small"
|
||||||
|
pagination={{ defaultPageSize: 20 }}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{detailModalVisible && (
|
||||||
|
<DetailModal
|
||||||
|
detail={currentPluginDetail}
|
||||||
|
onSubmit={onSavePlugin}
|
||||||
|
onCancel={() => {
|
||||||
|
setDetailModalVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginManage;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { request } from "umi";
|
||||||
|
import { DimensionType, DomainType, PluginType } from "./type";
|
||||||
|
|
||||||
|
export function savePlugin(params: Partial<PluginType>) {
|
||||||
|
return request<Result<any>>('/api/chat/plugin', {
|
||||||
|
method: params.id ? 'PUT' : 'POST',
|
||||||
|
data: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPluginList(filters?: any) {
|
||||||
|
return request<Result<any[]>>('/api/chat/plugin/query', {
|
||||||
|
method: 'POST',
|
||||||
|
data: filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePlugin(id: number) {
|
||||||
|
return request<Result<any>>(`/api/chat/plugin/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDomainList() {
|
||||||
|
return request<Result<DomainType[]>>('/api/chat/conf/domainList', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDimensionList(domainId: number) {
|
||||||
|
return request<Result<{list: DimensionType[]}>>('/api/semantic/dimension/queryDimension', {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
domainIds: [domainId],
|
||||||
|
current: 1,
|
||||||
|
pageSize: 2000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
.pluginManage {
|
||||||
|
.filterSection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 12px;
|
||||||
|
column-gap: 30px;
|
||||||
|
margin: 12px 24px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.filterItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 12px;
|
||||||
|
width: 22vw;
|
||||||
|
|
||||||
|
.filterItemTitle {
|
||||||
|
width: 60px;
|
||||||
|
margin-right: 6px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterItemControl {
|
||||||
|
// width: 20vw;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginList {
|
||||||
|
margin: 12px 24px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.titleBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.domainColumn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paramsSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 12px;
|
||||||
|
.filterRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 12px;
|
||||||
|
|
||||||
|
.filterParamName {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterParamValueField {
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionExample {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
webapp/packages/supersonic-fe/src/pages/ChatPlugin/type.ts
Normal file
68
webapp/packages/supersonic-fe/src/pages/ChatPlugin/type.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
export type PluginConfigType = {
|
||||||
|
url: string;
|
||||||
|
params: any;
|
||||||
|
paramOptions: any;
|
||||||
|
valueParams: any;
|
||||||
|
forwardParam: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PluginTypeEnum {
|
||||||
|
WEB_PAGE = 'WEB_PAGE',
|
||||||
|
WEB_SERVICE = 'WEB_SERVICE',
|
||||||
|
DSL = 'DSL'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ParseModeEnum {
|
||||||
|
EMBEDDING_RECALL = 'EMBEDDING_RECALL',
|
||||||
|
FUNCTION_CALL = 'FUNCTION_CALL'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ParamTypeEnum {
|
||||||
|
CUSTOM = 'CUSTOM',
|
||||||
|
SEMANTIC = 'SEMANTIC',
|
||||||
|
FORWARD = 'FORWARD'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginType = {
|
||||||
|
id: number;
|
||||||
|
type: PluginTypeEnum;
|
||||||
|
domainList: number[];
|
||||||
|
pattern: string;
|
||||||
|
parseMode: ParseModeEnum;
|
||||||
|
parseModeConfig: string;
|
||||||
|
name: string;
|
||||||
|
config: PluginConfigType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DomainType = {
|
||||||
|
id: number | string;
|
||||||
|
parentId: number;
|
||||||
|
name: string;
|
||||||
|
bizName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DimensionType = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
bizName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FunctionParamType = {
|
||||||
|
type: string;
|
||||||
|
properties: Record<string, { type: string, description: string }>;
|
||||||
|
required: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionType = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: FunctionParamType;
|
||||||
|
examples: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionParamFormItemType = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -422,3 +422,7 @@ export function traverseRoutes(routes, env: string, result: any[] = []) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isProd() {
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user