Merge pull request #35 from williamhliu/master

[feature](weaapp) add agent
This commit is contained in:
williamhliu
2023-08-20 18:47:39 +08:00
committed by GitHub
40 changed files with 1928 additions and 316 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "supersonic-chat-sdk",
"version": "0.3.0",
"version": "0.4.32",
"main": "dist/index.es.js",
"module": "dist/index.es.js",
"unpkg": "dist/index.umd.js",

View File

@@ -40,13 +40,13 @@ export const THEME_COLOR_LIST = [
'#5CA9E6',
];
export const PARSE_ERROR_TIP = '小Q不太懂您说什么呐,回去一定补充知识';
export const PARSE_ERROR_TIP = '智能助理不太懂您说什么呐,回去一定补充知识';
export const SEARCH_EXCEPTION_TIP = '查询出错啦,智能小Q还不够聪明,请您换个表达再试试';
export const SEARCH_EXCEPTION_TIP = '查询出错啦,智能助理还不够聪明,请您换个表达再试试';
export const MSG_VALID_TIP = {
[MsgValidTypeEnum.SEARCH_EXCEPTION]: '数据查询异常',
[MsgValidTypeEnum.INVALID]: '小Q不太懂您说什么呐,回去一定补充知识',
[MsgValidTypeEnum.INVALID]: '智能助理不太懂您说什么呐,回去一定补充知识',
};
export const PREFIX_CLS = 'ss-chat';

View File

@@ -51,10 +51,21 @@ export type FilterItemType = {
value: string[];
};
export type ModelType = {
alias: string;
bizName: string;
id: number;
model: number;
name: string;
type: string;
useCnt: number;
}
export type ChatContextType = {
aggType: string;
modelId: number;
modelName: string;
model: ModelType;
dateInfo: DateInfoType;
dimensions: FieldType[];
metrics: FieldType[];

View File

@@ -1,3 +1,4 @@
import { Spin } from 'antd';
import { PREFIX_CLS } from '../../common/constants';
import { MsgDataType } from '../../common/type';
import ChatMsg from '../ChatMsg';
@@ -8,6 +9,7 @@ import Typing from './Typing';
type Props = {
question: string;
executeLoading: boolean;
entitySwitchLoading: boolean;
chartIndex: number;
executeTip?: string;
data?: MsgDataType;
@@ -21,6 +23,7 @@ type Props = {
const ExecuteItem: React.FC<Props> = ({
question,
executeLoading,
entitySwitchLoading,
chartIndex,
executeTip,
data,
@@ -50,13 +53,15 @@ const ExecuteItem: React.FC<Props> = ({
return (
<div className={`${prefixCls}-msg-content`}>
<ChatMsg
question={question}
data={data}
chartIndex={chartIndex}
isMobileMode={isMobileMode}
triggerResize={triggerResize}
/>
<Spin spinning={entitySwitchLoading}>
<ChatMsg
question={question}
data={data}
chartIndex={chartIndex}
isMobileMode={isMobileMode}
triggerResize={triggerResize}
/>
</Spin>
{!isMetricCard && (
<Tools
data={data}

View File

@@ -76,17 +76,7 @@ const ParseTip: React.FC<Props> = ({
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 { type: agentType, name: agentName } = properties || {};
const fields =
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
@@ -101,11 +91,10 @@ const ParseTip: React.FC<Props> = ({
}}
>
{index !== undefined && <div>{index + 1}.</div>}
{!pluginName && isOptions && <div className={`${prefixCls}-mode-name`}>{modeName}</div>}
{!!pluginName ? (
{!!agentType ? (
<div className={`${prefixCls}-tip-item`}>
<span className={itemValueClass}>{pluginName}</span>
{agentType === 'plugin' ? '插件' : '内置'}
<span className={itemValueClass}>{agentName}</span>
</div>
) : (
<>
@@ -123,7 +112,7 @@ const ParseTip: React.FC<Props> = ({
<div className={itemValueClass}>{modelName}</div>
</div>
)}
{modeName === '算指标' && metric && (
{metric && (
<div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div>
<div className={itemValueClass}>{metric.name}</div>
@@ -153,9 +142,13 @@ const ParseTip: React.FC<Props> = ({
</div>
</div>
)}
{['METRIC_FILTER', 'METRIC_ENTITY', 'ENTITY_DETAIL', 'ENTITY_LIST_FILTER'].includes(
queryMode
) &&
{[
'METRIC_FILTER',
'METRIC_ENTITY',
'ENTITY_DETAIL',
'ENTITY_LIST_FILTER',
'ENTITY_ID',
].includes(queryMode) &&
dimensionFilters &&
dimensionFilters?.length > 0 && (
<div className={`${prefixCls}-tip-item`}>
@@ -198,10 +191,10 @@ const ParseTip: React.FC<Props> = ({
</div>
);
} else {
const pluginName = parseInfoOptions[0]?.properties?.CONTEXT?.plugin?.name;
const agentType = parseInfoOptions[0]?.properties?.type;
tipNode = (
<div className={`${prefixCls}-tip`}>
<div>{!!pluginName ? '您的问题' : '您的问题解析为:'}</div>
<div>{!!agentType ? '您的问题' : '您的问题解析为:'}</div>
{getTipNode(parseInfoOptions[0])}
</div>
);

View File

@@ -5,11 +5,14 @@ import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/
import IconFont from '../IconFont';
import ParseTip from './ParseTip';
import ExecuteItem from './ExecuteItem';
import { isMobile } from '../../utils/utils';
import classNames from 'classnames';
type Props = {
msg: string;
conversationId?: number;
modelId?: number;
agentId?: number;
filter?: any[];
isLastMessage?: boolean;
msgData?: MsgDataType;
@@ -24,6 +27,7 @@ const ChatItem: React.FC<Props> = ({
msg,
conversationId,
modelId,
agentId,
filter,
isLastMessage,
isMobileMode,
@@ -41,7 +45,7 @@ const ChatItem: React.FC<Props> = ({
const [executeLoading, setExecuteLoading] = useState(false);
const [executeTip, setExecuteTip] = useState('');
const [executeMode, setExecuteMode] = useState(false);
const [entitySwitching, setEntitySwitching] = useState(false);
const [entitySwitchLoading, setEntitySwitchLoading] = useState(false);
const [chartIndex, setChartIndex] = useState(0);
@@ -76,37 +80,42 @@ const ChatItem: React.FC<Props> = ({
) => {
setExecuteMode(true);
setExecuteLoading(true);
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
setExecuteLoading(false);
const valid = updateData(data);
if (onMsgDataLoaded) {
let parseOptions: ChatContextType[] = parseInfoOptions || [];
if (
parseInfoOptions &&
parseInfoOptions.length > 1 &&
(parseInfoOptions[0].queryMode.includes('METRIC') ||
parseInfoOptions[0].queryMode.includes('ENTITY'))
) {
parseOptions = parseInfoOptions.filter(
(item, index) =>
index === 0 ||
(!item.queryMode.includes('METRIC') && !item.queryMode.includes('ENTITY'))
try {
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
setExecuteLoading(false);
const valid = updateData(data);
if (onMsgDataLoaded) {
let parseOptions: ChatContextType[] = parseInfoOptions || [];
if (
parseInfoOptions &&
parseInfoOptions.length > 1 &&
(parseInfoOptions[0].queryMode.includes('METRIC') ||
parseInfoOptions[0].queryMode.includes('ENTITY'))
) {
parseOptions = parseInfoOptions.filter(
(item, index) =>
index === 0 ||
(!item.queryMode.includes('METRIC') && !item.queryMode.includes('ENTITY'))
);
}
onMsgDataLoaded(
{
...data.data,
chatContext: parseInfoValue,
parseOptions: parseOptions.length > 1 ? parseOptions.slice(1) : undefined,
},
valid
);
}
onMsgDataLoaded(
{
...data.data,
chatContext: parseInfoValue,
parseOptions: parseOptions.length > 1 ? parseOptions.slice(1) : undefined,
},
valid
);
} catch (e) {
setExecuteLoading(false);
setExecuteTip(SEARCH_EXCEPTION_TIP);
}
};
const onSendMsg = async () => {
setParseLoading(true);
const { data: parseData } = await chatParse(msg, conversationId, modelId, filter);
const { data: parseData } = await chatParse(msg, conversationId, modelId, agentId, filter);
setParseLoading(false);
const { code, data } = parseData || {};
const { state, selectedParses } = data || {};
@@ -115,10 +124,9 @@ const ChatItem: React.FC<Props> = ({
state === ParseStateEnum.FAILED ||
selectedParses == null ||
selectedParses.length === 0 ||
(selectedParses.length === 1 &&
!selectedParses[0]?.modelName &&
!selectedParses[0]?.properties?.CONTEXT?.plugin?.name &&
selectedParses[0]?.queryMode !== 'WEB_PAGE')
(selectedParses.length > 0 &&
!selectedParses[0]?.properties?.type &&
!selectedParses[0]?.queryMode)
) {
setParseTip(PARSE_ERROR_TIP);
return;
@@ -146,9 +154,9 @@ const ChatItem: React.FC<Props> = ({
}, [msg, msgData]);
const onSwitchEntity = async (entityId: string) => {
setEntitySwitching(true);
setEntitySwitchLoading(true);
const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
setEntitySwitching(false);
setEntitySwitchLoading(false);
setData(res.data.data);
};
@@ -164,11 +172,15 @@ const ChatItem: React.FC<Props> = ({
}
};
const contentClass = classNames(`${prefixCls}-content`, {
[`${prefixCls}-content-mobile`]: isMobile,
});
return (
<div className={prefixCls}>
<div className={`${prefixCls}-section`}>
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
<div className={`${prefixCls}-content`}>
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
<div className={contentClass}>
<ParseTip
parseLoading={parseLoading}
parseInfoOptions={parseOptions || parseInfoOptions.slice(0, 1)}
@@ -181,11 +193,12 @@ const ChatItem: React.FC<Props> = ({
</div>
{executeMode && data?.queryMode !== 'WEB_PAGE' && (
<div className={`${prefixCls}-section`}>
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
<div className={`${prefixCls}-content`}>
{!isMobile && <IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />}
<div className={contentClass}>
<ExecuteItem
question={msg}
executeLoading={executeLoading}
entitySwitchLoading={entitySwitchLoading}
executeTip={executeTip}
chartIndex={chartIndex}
data={data}

View File

@@ -117,6 +117,10 @@
width: calc(100% - 50px);
}
&-content-mobile {
width: 100%;
}
&-metric-info-list {
margin-top: 30px;
display: flex;
@@ -136,7 +140,6 @@
&-typing-bubble {
width: fit-content;
// padding: 16px !important;
}
&-text-bubble {

View File

@@ -4,6 +4,7 @@ import { PREFIX_CLS } from '../../../common/constants';
type Props = {
position: 'left' | 'right';
width?: number | string;
maxWidth?: number | string;
height?: number | string;
title?: string;
followQuestions?: string[];
@@ -17,6 +18,7 @@ type Props = {
const Message: React.FC<Props> = ({
width,
maxWidth,
height,
children,
bubbleClassName,
@@ -38,7 +40,7 @@ const Message: React.FC<Props> = ({
<div className={`${prefixCls}-body`}>
<div
className={`${prefixCls}-bubble${bubbleClassName ? ` ${bubbleClassName}` : ''}`}
style={{ width, height }}
style={{ width, height, maxWidth }}
onClick={e => {
e.stopPropagation();
}}

View File

@@ -43,7 +43,11 @@ const MetricCard: React.FC<Props> = ({
return (
<div className={prefixCls}>
<div className={`${prefixCls}-top-bar`}>
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
{indicatorColumn?.name ? (
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
) : (
<div style={{ height: 32 }} />
)}
{(hasFilterSection || drillDownDimension) && (
<div className={`${prefixCls}-filter-section-wrapper`}>
(

View File

@@ -109,7 +109,7 @@
&-drill-down-dimensions {
position: absolute;
bottom: -38px;
left: 0;
bottom: -44px;
left: -16;
}
}

View File

@@ -19,25 +19,19 @@ type Props = {
};
const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApplyAuth }) => {
const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data;
const { dateMode, unit } = chatContext?.dateInfo || {};
const { entityInfo, chatContext, queryMode } = data;
const { dateInfo, dimensionFilters, elementMatches } = chatContext || {};
const { dateMode, unit } = dateInfo || {};
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 [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
const [dataSource, setDataSource] = useState<any[]>(queryResults);
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
const [dimensions, setDimensions] = useState<FieldType[]>(chatContext?.dimensions);
const [columns, setColumns] = useState<ColumnType[]>([]);
const [activeMetricField, setActiveMetricField] = useState<FieldType>();
const [dataSource, setDataSource] = useState<any[]>([]);
const [currentDateOption, setCurrentDateOption] = useState<number>();
const [dimensions, setDimensions] = useState<FieldType[]>();
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
const [aggregateInfoValue, setAggregateInfoValue] = useState<any>(aggregateInfo);
const [dateModeValue, setDateModeValue] = useState(dateMode);
const [aggregateInfoValue, setAggregateInfoValue] = useState<any>();
const [dateModeValue, setDateModeValue] = useState<any>();
const [loading, setLoading] = useState(false);
const dateField: any = columns.find(
@@ -47,9 +41,31 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
const categoryColumnName =
columns.find((column: any) => column.showType === 'CATEGORY')?.nameEn || '';
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
const entityName = elementMatches?.find((item: any) => item.element?.type === 'ID')?.element
?.name;
const isEntityMode =
(queryMode === 'ENTITY_LIST_FILTER' || queryMode === 'METRIC_ENTITY') &&
typeof entityId === 'string' &&
entityName !== undefined;
useEffect(() => {
const { queryColumns, queryResults, chatContext, aggregateInfo } = data;
const initialDateOption = dateOptions.find((option: any) => {
return dateMode === 'RECENT' && option.value === unit;
})?.value;
setColumns(queryColumns || []);
setActiveMetricField(chatContext?.metrics?.[0]);
setDataSource(queryResults);
}, [queryResults]);
setCurrentDateOption(initialDateOption);
setDimensions(chatContext?.dimensions);
setDrillDownDimension(undefined);
setAggregateInfoValue(aggregateInfo);
setDateModeValue(chatContext?.dateInfo?.dateMode);
}, [data]);
useEffect(() => {
if (queryMode === 'METRIC_GROUPBY') {
@@ -117,14 +133,14 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
});
};
const currentMetricField = columns.find((column: any) => column.showType === 'NUMBER');
if (!currentMetricField) {
return null;
}
const prefixCls = `${CLS_PREFIX}-metric-trend`;
const { dimensionFilters } = chatContext || {};
const hasFilterSection = dimensionFilters?.length > 0;
return (
@@ -174,59 +190,61 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
</div>
)}
</div>
{aggregateInfoValue?.metricInfos?.length > 0 && (
<MetricInfo aggregateInfo={aggregateInfoValue} />
)}
<div className={`${prefixCls}-date-options`}>
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
[`${prefixCls}-date-active`]: dateOption.value === currentDateOption,
[`${prefixCls}-date-mobile`]: isMobile,
});
return (
<>
<div
key={dateOption.value}
className={dateOptionClass}
onClick={() => {
selectDateOption(dateOption.value);
}}
>
{dateOption.label}
{dateOption.value === currentDateOption && (
<div className={`${prefixCls}-active-identifier`} />
)}
</div>
{index !== dateOptions.length - 1 && (
<div className={`${prefixCls}-date-option-divider`} />
)}
</>
);
})}
</div>
<Spin spinning={loading}>
{dataSource?.length === 1 || chartIndex % 2 === 1 ? (
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
) : (
<MetricTrendChart
model={entityInfo?.modelInfo.name}
dateColumnName={dateColumnName}
categoryColumnName={categoryColumnName}
metricField={currentMetricField}
resultList={dataSource}
triggerResize={triggerResize}
onApplyAuth={onApplyAuth}
<div className={`${prefixCls}-content`}>
{aggregateInfoValue?.metricInfos?.length > 0 && (
<MetricInfo aggregateInfo={aggregateInfoValue} />
)}
<div className={`${prefixCls}-date-options`}>
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
[`${prefixCls}-date-active`]: dateOption.value === currentDateOption,
[`${prefixCls}-date-mobile`]: isMobile,
});
return (
<>
<div
key={dateOption.value}
className={dateOptionClass}
onClick={() => {
selectDateOption(dateOption.value);
}}
>
{dateOption.label}
{dateOption.value === currentDateOption && (
<div className={`${prefixCls}-active-identifier`} />
)}
</div>
{index !== dateOptions.length - 1 && (
<div className={`${prefixCls}-date-option-divider`} />
)}
</>
);
})}
</div>
{dataSource?.length === 1 || chartIndex % 2 === 1 ? (
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
) : (
<MetricTrendChart
model={entityInfo?.modelInfo.name}
dateColumnName={dateColumnName}
categoryColumnName={categoryColumnName}
metricField={currentMetricField}
resultList={dataSource}
triggerResize={triggerResize}
onApplyAuth={onApplyAuth}
/>
)}
</div>
{queryMode.includes('METRIC') && !isEntityMode && (
<DrillDownDimensions
modelId={chatContext.modelId}
drillDownDimension={drillDownDimension}
dimensionFilters={chatContext.dimensionFilters}
onSelectDimension={onSelectDimension}
/>
)}
</Spin>
{queryMode.includes('METRIC') && (
<DrillDownDimensions
modelId={chatContext.modelId}
drillDownDimension={drillDownDimension}
dimensionFilters={chatContext.dimensionFilters}
onSelectDimension={onSelectDimension}
/>
)}
</div>
</div>
);

View File

@@ -48,6 +48,13 @@
font-weight: 500;
}
&-content {
display: flex;
flex-direction: column;
width: 100%;
row-gap: 12px;
}
&-indicator {
display: flex;
flex-direction: column;

View File

@@ -5,7 +5,7 @@ import MetricCard from './MetricCard';
import MetricTrend from './MetricTrend';
import Table from './Table';
import { ColumnType, DrillDownDimensionType, MsgDataType } from '../../common/type';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { queryData } from '../../service';
type Props = {
@@ -25,6 +25,11 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
const [loading, setLoading] = useState(false);
useEffect(() => {
setColumns(queryColumns);
setDataSource(queryResults);
}, [queryColumns, queryResults]);
if (!queryColumns || !queryResults) {
return null;
}
@@ -35,14 +40,15 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
const metricFields = columns.filter(item => item.showType === 'NUMBER');
const isMetricCard =
queryMode.includes('METRIC') &&
(queryMode.includes('METRIC') ||
(queryMode === 'DSL' && singleData && metricFields.length === 1 && columns.length === 1)) &&
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
const isText =
columns.length === 1 &&
columns[0].showType === 'CATEGORY' &&
!queryMode.includes('METRIC') &&
!queryMode.includes('ENTITY') &&
((!queryMode.includes('METRIC') && !queryMode.includes('ENTITY')) ||
queryMode === 'METRIC_INTERPRET') &&
singleData;
const onLoadData = async (value: any) => {
@@ -68,9 +74,43 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
const getMsgContent = () => {
if (isText) {
let text = dataSource[0][columns[0].nameEn];
let htmlCode: string;
const match = text.match(/```html([\s\S]*?)```/);
htmlCode = match && match[1].trim();
if (htmlCode) {
text = text.replace(/```html([\s\S]*?)```/, '');
}
let scriptCode: string;
let scriptSrc: string;
if (htmlCode) {
scriptSrc = htmlCode.match(/<script src="([\s\S]*?)"><\/script>/)?.[1] || '';
scriptCode =
htmlCode.match(/<script type="text\/javascript">([\s\S]*?)<\/script>/)?.[1] || '';
if (scriptSrc) {
const script = document.createElement('script');
script.src = scriptSrc;
document.body.appendChild(script);
}
if (scriptCode) {
const script = document.createElement('script');
script.innerHTML = scriptCode;
setTimeout(() => {
document.body.appendChild(script);
}, 1500);
}
}
return (
<div style={{ lineHeight: '24px', width: 'fit-content' }}>
{dataSource[0][columns[0].nameEn]}
<div
style={{
lineHeight: '24px',
width: 'fit-content',
maxWidth: '100%',
overflowX: 'hidden',
}}
>
{htmlCode ? <pre>{text}</pre> : text}
{!!htmlCode && <div dangerouslySetInnerHTML={{ __html: htmlCode }} />}
</div>
);
}
@@ -103,15 +143,18 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
);
}
}
return (
<Bar
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
triggerResize={triggerResize}
loading={loading}
drillDownDimension={drillDownDimension}
onSelectDimension={onSelectDimension}
/>
);
if (categoryField?.length > 0 && metricFields?.length > 0) {
return (
<Bar
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
triggerResize={triggerResize}
loading={loading}
drillDownDimension={drillDownDimension}
onSelectDimension={onSelectDimension}
/>
);
}
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
};
let width = '100%';
@@ -135,6 +178,7 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
title={question}
isMobileMode={isMobileMode}
width={width}
maxWidth={isText && !isMobile ? '80%' : undefined}
queryMode={queryMode}
>
{getMsgContent()}

View File

@@ -29,6 +29,15 @@ const Tools: React.FC<Props> = ({
const { queryColumns, queryResults, queryId, chatContext, queryMode, entityInfo } = data || {};
const [score, setScore] = useState(scoreValue || 0);
const { dimensionFilters, elementMatches } = data.chatContext;
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
const entityName = elementMatches?.find((item: any) => item.element?.type === 'ID')?.element
?.name;
const isEntityMode =
queryMode === 'ENTITY_LIST_FILTER' && typeof entityId === 'string' && entityName !== undefined;
const prefixCls = `${CLS_PREFIX}-tools`;
const singleData = queryResults.length === 1;
@@ -41,7 +50,8 @@ const Tools: React.FC<Props> = ({
queryColumns[0].showType === 'CATEGORY' &&
queryResults?.length === 1) ||
(!queryMode.includes('METRIC') && !queryMode.includes('ENTITY')) ||
isMetricCard;
isMetricCard ||
isEntityMode;
const changeChart = () => {
onChangeChart();
@@ -75,7 +85,7 @@ const Tools: React.FC<Props> = ({
return (
<div className={prefixCls}>
{/* {isLastMessage && chatContext?.modelId && entityInfo?.entityId && (
{isLastMessage && chatContext?.modelId && entityInfo?.entityId && (
<Popover
content={
<RecommendOptions
@@ -93,7 +103,7 @@ const Tools: React.FC<Props> = ({
>
<Button shape="round"></Button>
</Popover>
)} */}
)}
{!isMobile && (
<>
{queryMode === 'METRIC_FILTER' && (

View File

@@ -55,6 +55,7 @@ const Chat = () => {
<ChatItem
msg={msg}
// msgData={data}
agentId={6}
onMsgDataLoaded={onMsgDataLoaded}
isLastMessage
isMobileMode

View File

@@ -7,7 +7,7 @@ const axiosInstance: AxiosInstance = axios.create({
// 设置基本URL所有请求都会使用这个URL作为前缀
baseURL: '',
// 设置请求超时时间(毫秒)
timeout: 60000,
timeout: 120000,
// 设置请求头
headers: {
'Content-Type': 'application/json',

View File

@@ -25,11 +25,12 @@ export function chatQuery(queryText: string, chatId?: number, modelId?: number,
});
}
export function chatParse(queryText: string, chatId?: number, modelId?: number, filters?: any[]) {
export function chatParse(queryText: string, chatId?: number, modelId?: number, agentId?: number, filters?: any[]) {
return axios.post<Result<ParseDataType>>(`${prefix}/chat/query/parse`, {
queryText,
chatId: chatId || DEFAULT_CHAT_ID,
modelId,
agentId,
queryFilters: filters ? {
filters
} : undefined,
@@ -63,10 +64,6 @@ export function queryContext(queryText: string, chatId?: number) {
});
}
export function querySuggestionInfo(modelId: number) {
return axios.get<Result<any>>(`${prefix}/chat/recommend/${modelId}`);
}
export function getHistoryMsg(current: number, chatId: number = DEFAULT_CHAT_ID, pageSize: number = 10) {
return axios.post<Result<HistoryType>>(`${prefix}/chat/manage/pageQueryInfo?chatId=${chatId}`, {
current,
@@ -74,22 +71,6 @@ export function getHistoryMsg(current: number, chatId: number = DEFAULT_CHAT_ID,
});
}
export function queryMetricInfo(data: any) {
return axios.get(`/semantic/metric/getMetric/${data.classId}/${data.uniqueId}`);
}
export function getRelatedDimensionFromStatInfo(data: any) {
return axios.get(
`/semantic/metric/getRelatedDimensionFromStatInfo/${data.classId}/${data.uniqueId}`,
);
}
export function getMetricQueryInfo(data: any) {
return axios.get<any>(
`/openapi/bd-bi/api/polaris/intelligentQuery/getMetricQueryInfo/${data.classId}/${data.metricName}`
);
}
export function saveConversation(chatName: string) {
return axios.post<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`);
}

View File

@@ -26,6 +26,12 @@ const ROUTES = [
component: './ChatPlugin',
envEnableList: [ENV_KEY.CHAT],
},
{
path: '/agent',
name: 'agent',
component: './Agent',
envEnableList: [ENV_KEY.CHAT],
},
{
path: '/semanticModel/model/:domainId?/:modelId?/:menuKey?',
component: './SemanticModel/DomainManager',

View File

@@ -96,7 +96,7 @@
"react-split-pane": "^2.0.3",
"react-syntax-highlighter": "^15.4.3",
"sql-formatter": "^2.3.3",
"supersonic-chat-sdk": "^0.3.0",
"supersonic-chat-sdk": "^0.4.32",
"umi": "3.5",
"umi-request": "^1.0.8"
},

View File

@@ -20,7 +20,7 @@ const TOKEN_KEY = AUTH_TOKEN_KEY;
const replaceRoute = '/';
const getRuningEnv = async () => {
const getRunningEnv = async () => {
try {
const response = await fetch(`${publicPath}supersonic.config.json`);
const config = await response.json();
@@ -72,7 +72,6 @@ export async function getInitialState(): Promise<{
codeList?: string[];
authCodes?: string[];
}> {
// await getRuningEnv();
const fetchUserInfo = async () => {
try {
const { code, data } = await queryCurrentUser();
@@ -112,8 +111,9 @@ export async function getInitialState(): Promise<{
}
export async function patchRoutes({ routes }) {
const config = await getRuningEnv();
const config = await getRunningEnv();
if (config && config.env) {
window.RUNNING_ENV = config.env;
const { env } = config;
const target = routes[0].routes;
if (env) {
@@ -123,6 +123,14 @@ export async function patchRoutes({ routes }) {
// 写入根据环境转换过的的route
target.push(...envRoutes);
}
} else {
const target = routes[0].routes;
// start-standalone模式不存在env在此模式下不显示chatSetting
const envRoutes = target.filter((item: any) => {
return !['chatSetting'].includes(item.name);
});
target.splice(0, 99);
target.push(...envRoutes);
}
}

View File

@@ -13,4 +13,5 @@ export default {
'menu.chatPlugin': '问答插件',
'menu.login': '登录',
'menu.chat': '问答对话',
'menu.agent': '问答助理'
};

View File

@@ -0,0 +1,109 @@
import { Form, Modal, Input, Button, Switch } from 'antd';
import { AgentType } from './type';
import { useEffect, useState } from 'react';
import styles from './style.less';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { uuid } from '@/utils/utils';
const FormItem = Form.Item;
const { TextArea } = Input;
type Props = {
editAgent?: AgentType;
onSaveAgent: (agent: AgentType) => Promise<void>;
onCancel: () => void;
};
const AgentModal: React.FC<Props> = ({ editAgent, onSaveAgent, onCancel }) => {
const [saveLoading, setSaveLoading] = useState(false);
const [examples, setExamples] = useState<{ id: string; question?: string }[]>([]);
const [form] = Form.useForm();
useEffect(() => {
if (editAgent) {
form.setFieldsValue({ ...editAgent, enableSearch: editAgent.enableSearch !== 0 });
if (editAgent.examples) {
setExamples(editAgent.examples.map((question) => ({ id: uuid(), question })));
}
} else {
form.resetFields();
}
}, [editAgent]);
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
const onOk = async () => {
const values = await form.validateFields();
setSaveLoading(true);
await onSaveAgent({
id: editAgent?.id,
...(editAgent || {}),
...values,
examples: examples.map((example) => example.question),
enableSearch: values.enableSearch ? 1 : 0,
});
setSaveLoading(false);
};
return (
<Modal
open
title={editAgent ? '编辑助理' : '新建助理'}
confirmLoading={saveLoading}
width={800}
onOk={onOk}
onCancel={onCancel}
>
<Form {...layout} form={form} initialValues={{ enableSearch: true }}>
<FormItem name="name" label="名称" rules={[{ required: true, message: '请输入助理名称' }]}>
<Input placeholder="请输入助理名称" />
</FormItem>
<FormItem name="enableSearch" label="支持联想" valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
</FormItem>
<FormItem name="examples" 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 name="description" label="描述">
<TextArea placeholder="请输入助理描述" />
</FormItem>
</Form>
</Modal>
);
};
export default AgentModal;

View File

@@ -0,0 +1,144 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Input, Popconfirm, Switch } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import styles from './style.less';
import { AgentType } from './type';
const { Search } = Input;
type Props = {
agents: AgentType[];
currentAgent?: AgentType;
loading: boolean;
onSelectAgent: (agent: AgentType) => void;
onDeleteAgent: (id: number) => void;
onEditAgent: (agent?: AgentType) => void;
onSaveAgent: (agent: AgentType, noTip?: boolean) => Promise<void>;
};
const AgentsSection: React.FC<Props> = ({
agents,
currentAgent,
onSelectAgent,
onDeleteAgent,
onEditAgent,
onSaveAgent,
}) => {
// const [searchName, setSearchName] = useState('');
const [showAgents, setShowAgents] = useState<AgentType[]>([]);
useEffect(() => {
setShowAgents(agents);
}, [agents]);
return (
<div className={styles.agentsSection}>
{/* <div className={styles.sectionTitle}>问答助理</div> */}
<div className={styles.content}>
<div className={styles.searchBar}>
{/* <Search
placeholder="请输入助理名称搜索"
className={styles.searchControl}
value={searchName}
onChange={(e) => {
setSearchName(e.target.value);
}}
onSearch={(value) => {
setShowAgents(
agents.filter((agent) =>
agent.name?.trim().toLowerCase().includes(value.trim().toLowerCase()),
),
);
}}
/> */}
<Button
type="primary"
onClick={() => {
onEditAgent(undefined);
}}
>
<PlusOutlined />
</Button>
</div>
<div className={styles.agentsContainer}>
{showAgents.map((agent) => {
const agentItemClass = classNames(styles.agentItem, {
[styles.agentActive]: agent.id === currentAgent?.id,
});
return (
<div
className={agentItemClass}
key={agent.id}
onClick={() => {
onSelectAgent(agent);
}}
>
<UserOutlined className={styles.agentIcon} />
<div className={styles.agentContent}>
<div className={styles.agentNameBar}>
<div className={styles.agentName}>{agent.name}</div>
<div className={styles.operateIcons}>
<EditOutlined
className={styles.operateIcon}
onClick={(e) => {
e.stopPropagation();
onEditAgent(agent);
}}
/>
<Popconfirm
title="确定删除吗?"
onCancel={(e) => {
e?.stopPropagation();
}}
onConfirm={(e) => {
e?.stopPropagation();
onDeleteAgent(agent.id!);
}}
>
<DeleteOutlined
className={styles.operateIcon}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
</div>
</div>
<div className={styles.bottomBar}>
<div className={styles.agentDescription} title={agent.description}>
{agent.description}
</div>
<div className={styles.toggleStatus}>
{agent.status === 0 ? (
'已禁用'
) : (
<span className={styles.online}></span>
)}
<span
onClick={(e) => {
e.stopPropagation();
}}
>
<Switch
size="small"
defaultChecked={agent.status === 1}
onChange={(value) => {
onSaveAgent({ ...agent, status: value ? 1 : 0 }, true);
}}
/>
</span>
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export default AgentsSection;

View File

@@ -0,0 +1,264 @@
import { Form, Modal, Input, Select, Button } from 'antd';
import {
AgentToolType,
AgentToolTypeEnum,
AGENT_TOOL_TYPE_LIST,
MetricOptionType,
MetricType,
ModelType,
QUERY_MODE_LIST,
} from './type';
import { useEffect, useState } from 'react';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import styles from './style.less';
import { getLeafList, uuid } from '@/utils/utils';
import { getMetricList, getModelList } from './service';
import { PluginType } from '../ChatPlugin/type';
import { getPluginList } from '../ChatPlugin/service';
const FormItem = Form.Item;
type Props = {
editTool?: AgentToolType;
onSaveTool: (tool: AgentToolType) => Promise<void>;
onCancel: () => void;
};
const ToolModal: React.FC<Props> = ({ editTool, onSaveTool, onCancel }) => {
const [toolType, setToolType] = useState<AgentToolTypeEnum>();
const [modelList, setModelList] = useState<ModelType[]>([]);
const [saveLoading, setSaveLoading] = useState(false);
const [examples, setExamples] = useState<{ id: string; question?: string }[]>([]);
const [metricOptions, setMetricOptions] = useState<MetricOptionType[]>([]);
const [modelMetricList, setModelMetricList] = useState<MetricType[]>([]);
const [plugins, setPlugins] = useState<PluginType[]>([]);
const [form] = Form.useForm();
const initModelList = async () => {
const res = await getModelList();
setModelList([{ id: -1, name: '默认' }, ...getLeafList(res.data)]);
};
const initPluginList = async () => {
const res = await getPluginList({});
setPlugins(res.data || []);
};
useEffect(() => {
initModelList();
initPluginList();
}, []);
const initModelMetrics = async (params: any) => {
const res = await getMetricList(params[0].modelId);
setModelMetricList(res.data.list);
};
useEffect(() => {
if (editTool) {
form.setFieldsValue({ ...editTool, plugins: editTool.plugins?.[0] });
setToolType(editTool.type);
setExamples(
(editTool.exampleQuestions || []).map((item) => ({ id: uuid(), question: item })),
);
setMetricOptions(editTool.metricOptions || []);
if (editTool.metricOptions && editTool.metricOptions.length > 0) {
initModelMetrics(editTool.metricOptions || []);
}
} else {
form.resetFields();
}
}, [editTool]);
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
const onOk = async () => {
const values = await form.validateFields();
setSaveLoading(true);
await onSaveTool({
id: editTool?.id,
...values,
exampleQuestions: examples.map((item) => item.question).filter((item) => item),
plugins: values.plugins ? [values.plugins] : undefined,
metricOptions: metricOptions.map((item) => ({ modelId: values.modelId, ...item })),
});
setSaveLoading(false);
};
const updateMetricList = async (value: number) => {
if (modelMetricList[value]) {
return;
}
const res = await getMetricList(value);
setModelMetricList(res.data.list);
};
return (
<Modal
open
title={editTool ? '编辑工具' : '新建工具'}
confirmLoading={saveLoading}
width={800}
onOk={onOk}
onCancel={onCancel}
>
<Form {...layout} form={form}>
<FormItem name="type" label="类型" rules={[{ required: true, message: '请选择工具类型' }]}>
<Select
options={AGENT_TOOL_TYPE_LIST}
placeholder="请选择工具类型"
onChange={setToolType}
/>
</FormItem>
<FormItem name="name" label="名称">
<Input placeholder="请输入工具名称" />
</FormItem>
{toolType === AgentToolTypeEnum.DSL && (
<>
<FormItem name="modelIds" label="主题域">
<Select
options={modelList.map((model) => ({ label: model.name, value: model.id }))}
placeholder="请选择主题域"
mode="multiple"
/>
</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>
</>
)}
{toolType === AgentToolTypeEnum.INTERPRET && (
<>
<FormItem name="modelId" label="主题域">
<Select
options={modelList.map((model) => ({ label: model.name, value: model.id }))}
showSearch
filterOption={(input, option) =>
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
}
placeholder="请选择主题域"
onChange={(value) => {
setMetricOptions([...metricOptions]);
updateMetricList(value);
}}
/>
</FormItem>
<FormItem name="params" label="指标">
<div className={styles.paramsSection}>
{metricOptions.map((filter: any) => {
return (
<div className={styles.filterRow} key={filter.id}>
<Select
placeholder="请选择指标,需先选择主题域"
options={(modelMetricList || []).map((metric) => ({
label: metric.name,
value: `${metric.id}`,
}))}
showSearch
className={styles.filterParamValueField}
filterOption={(input, option) =>
((option?.label ?? '') as string)
.toLowerCase()
.includes(input.toLowerCase())
}
allowClear
value={filter.metricId}
onChange={(value) => {
filter.metricId = value;
setMetricOptions([...metricOptions]);
}}
/>
<DeleteOutlined
onClick={() => {
setMetricOptions(metricOptions.filter((item) => item.id !== filter.id));
}}
/>
</div>
);
})}
<Button
onClick={() => {
setMetricOptions([
...metricOptions,
{ id: uuid(), metricId: undefined, modelId: undefined },
]);
}}
>
<PlusOutlined />
</Button>
</div>
</FormItem>
</>
)}
{toolType === AgentToolTypeEnum.PLUGIN && (
<FormItem name="plugins" label="插件">
<Select
placeholder="请选择插件"
options={plugins.map((plugin) => ({ label: plugin.name, value: plugin.id }))}
showSearch
filterOption={(input, option) =>
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
}
onChange={(value) => {
const plugin = plugins.find((item) => item.id === value);
if (plugin) {
form.setFieldsValue({ name: plugin.name });
}
}}
/>
</FormItem>
)}
{toolType === AgentToolTypeEnum.RULE && (
<FormItem name="queryModes" label="查询模式">
<Select
placeholder="请选择查询模式"
options={QUERY_MODE_LIST}
showSearch
mode="multiple"
filterOption={(input, option) =>
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
}
/>
</FormItem>
)}
</Form>
</Modal>
);
};
export default ToolModal;

View File

@@ -0,0 +1,196 @@
import { uuid } from '@/utils/utils';
import {
ArrowLeftOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
ToolOutlined,
} from '@ant-design/icons';
import { Button, Empty, Popconfirm, Space, Switch, Tag } from 'antd';
import { useState } from 'react';
import styles from './style.less';
import ToolModal from './ToolModal';
import { AgentToolType, AgentType, AGENT_TOOL_TYPE_LIST } from './type';
type Props = {
currentAgent?: AgentType;
onSaveAgent: (agent: AgentType, noTip?: boolean) => Promise<void>;
onEditAgent: (agent?: AgentType) => void;
goBack: () => void;
};
const ToolsSection: React.FC<Props> = ({ currentAgent, onSaveAgent, onEditAgent, goBack }) => {
const [modalVisible, setModalVisible] = useState(false);
const [editTool, setEditTool] = useState<AgentToolType>();
const agentConfig = currentAgent?.agentConfig ? JSON.parse(currentAgent.agentConfig as any) : {};
const saveAgent = async (agent: AgentType) => {
await onSaveAgent(agent);
setModalVisible(false);
};
const onSaveTool = async (tool: AgentToolType) => {
const newAgentConfig = agentConfig || ({} as any);
if (!newAgentConfig.tools) {
newAgentConfig.tools = [];
}
if (tool.id) {
const index = newAgentConfig.tools.findIndex((item: AgentToolType) => item.id === tool.id);
newAgentConfig.tools[index] = tool;
} else {
newAgentConfig.tools.push({ ...tool, id: uuid() });
}
await saveAgent({
...currentAgent,
agentConfig: JSON.stringify(newAgentConfig) as any,
});
setModalVisible(false);
};
const onDeleteTool = async (tool: AgentToolType) => {
const newAgentConfig = agentConfig || ({} as any);
if (!newAgentConfig.tools) {
newAgentConfig.tools = [];
}
newAgentConfig.tools = newAgentConfig.tools.filter(
(item: AgentToolType) => item.id !== tool.id,
);
await saveAgent({
...currentAgent,
agentConfig: JSON.stringify(newAgentConfig) as any,
});
};
return (
<div className={styles.toolsSection}>
<div className={styles.toolsSectionTitleBar}>
<ArrowLeftOutlined className={styles.backIcon} onClick={goBack} />
<div className={styles.agentTitle}>{currentAgent?.name}</div>
<div className={styles.toggleStatus}>
{currentAgent?.status === 0 ? '已禁用' : <span className={styles.online}></span>}
<span
onClick={(e) => {
e.stopPropagation();
}}
>
<Switch
size="small"
defaultChecked={currentAgent?.status === 1}
onChange={(value) => {
onSaveAgent({ ...currentAgent, status: value ? 1 : 0 }, true);
}}
/>
</span>
</div>
</div>
<div className={styles.basicInfo}>
<div className={styles.basicInfoTitle}>
<Button
type="primary"
onClick={() => {
onEditAgent(currentAgent);
}}
>
</Button>
</div>
<div className={styles.infoContent}>
<div className={styles.infoItem}>
<Space>
{currentAgent?.examples?.map((item) => (
<Tag key={item}>{item}</Tag>
))}
</Space>
</div>
<div className={styles.infoItem}>{currentAgent?.description}</div>
</div>
</div>
<div className={styles.toolSection}>
<div className={styles.toolSectionTitleBar}>
<div className={styles.toolSectionTitle}></div>
<Button
type="primary"
onClick={() => {
setEditTool(undefined);
setModalVisible(true);
}}
>
<PlusOutlined />
</Button>
</div>
{agentConfig?.tools && agentConfig?.tools?.length > 0 ? (
<div className={styles.toolsContent}>
{agentConfig.tools.map((tool: AgentToolType) => {
const toolType = AGENT_TOOL_TYPE_LIST.find((item) => item.value === tool.type)?.label;
return (
<div
className={styles.toolItem}
key={tool.id}
onClick={() => {
setEditTool(tool);
setModalVisible(true);
}}
>
<ToolOutlined className={styles.toolIcon} />
<div className={styles.toolContent}>
<div className={styles.toolTopSection}>
<div className={styles.toolType}>{tool.name || toolType}</div>
<div className={styles.toolOperateIcons}>
<EditOutlined
className={styles.toolOperateIcon}
onClick={(e) => {
e.stopPropagation();
setEditTool(tool);
setModalVisible(true);
}}
/>
<Popconfirm
title="确定删除吗?"
onCancel={(e) => {
e?.stopPropagation();
}}
onConfirm={(e) => {
e?.stopPropagation();
onDeleteTool(tool);
}}
>
<DeleteOutlined
className={styles.toolOperateIcon}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
</div>
</div>
<div className={styles.toolDesc} title={toolType}>
{toolType}
</div>
</div>
</div>
);
})}
</div>
) : (
<div className={styles.emptyHolder}>
<Empty description={`${currentAgent?.name}】暂无工具,请新增工具`} />
</div>
)}
</div>
{modalVisible && (
<ToolModal
editTool={editTool}
onSaveTool={onSaveTool}
onCancel={() => {
setModalVisible(false);
}}
/>
)}
</div>
);
};
export default ToolsSection;

View File

@@ -0,0 +1,94 @@
import { message } from 'antd';
import { useEffect, useState } from 'react';
import AgentsSection from './AgentsSection';
import AgentModal from './AgentModal';
import { deleteAgent, getAgentList, saveAgent } from './service';
import styles from './style.less';
import ToolsSection from './ToolsSection';
import { AgentType } from './type';
const Agent = () => {
const [agents, setAgents] = useState<AgentType[]>([]);
const [loading, setLoading] = useState(false);
const [currentAgent, setCurrentAgent] = useState<AgentType>();
const [modalVisible, setModalVisible] = useState(false);
const [editAgent, setEditAgent] = useState<AgentType>();
const updateData = async () => {
setLoading(true);
const res = await getAgentList();
setLoading(false);
setAgents(res.data || []);
if (!res.data?.length) {
return;
}
if (currentAgent) {
const agent = res.data.find((item) => item.id === currentAgent.id);
if (agent) {
setCurrentAgent(agent);
} else {
setCurrentAgent(res.data[0]);
}
}
};
useEffect(() => {
updateData();
}, []);
const onSaveAgent = async (agent: AgentType, noTip?: boolean) => {
await saveAgent(agent);
if (!noTip) {
message.success('保存成功');
}
setModalVisible(false);
updateData();
};
const onDeleteAgent = async (id: number) => {
await deleteAgent(id);
message.success('删除成功');
updateData();
};
const onEditAgent = (agent?: AgentType) => {
setEditAgent(agent);
setModalVisible(true);
};
return (
<div className={styles.agent}>
{!currentAgent ? (
<AgentsSection
agents={agents}
currentAgent={currentAgent}
loading={loading}
onSelectAgent={setCurrentAgent}
onEditAgent={onEditAgent}
onDeleteAgent={onDeleteAgent}
onSaveAgent={onSaveAgent}
/>
) : (
<ToolsSection
currentAgent={currentAgent}
onSaveAgent={onSaveAgent}
onEditAgent={onEditAgent}
goBack={() => {
setCurrentAgent(undefined);
}}
/>
)}
{modalVisible && (
<AgentModal
editAgent={editAgent}
onSaveAgent={onSaveAgent}
onCancel={() => {
setModalVisible(false);
}}
/>
)}
</div>
);
};
export default Agent;

View File

@@ -0,0 +1,36 @@
import { request } from "umi";
import { AgentType, MetricType, ModelType } from "./type";
export function getAgentList() {
return request<Result<AgentType[]>>('/api/chat/agent/getAgentList');
}
export function saveAgent(agent: AgentType) {
return request<Result<any>>('/api/chat/agent', {
method: agent?.id ? 'PUT' : 'POST',
data: {...agent, status: agent.status !== undefined ? agent.status : 1},
});
}
export function deleteAgent(id: number) {
return request<Result<any>>(`/api/chat/agent/${id}`, {
method: 'DELETE',
});
}
export function getModelList() {
return request<Result<ModelType[]>>('/api/chat/conf/modelList', {
method: 'GET',
});
}
export function getMetricList(modelId: number) {
return request<Result<{list: MetricType[]}>>('/api/semantic/metric/queryMetric', {
method: 'POST',
data: {
modelIds: [modelId],
current: 1,
pageSize: 2000
}
});
}

View File

@@ -0,0 +1,292 @@
.agent {
// background: #fff;
height: calc(100vh - 48px);
}
.agentsSection {
padding: 20px 40px;
background: #fff;
height: calc(100vh - 48px);
.sectionTitle {
font-size: 24px;
font-weight: 700;
padding-bottom: 30px;
}
}
.content {
margin-top: 20px;
.searchBar {
display: flex;
align-items: center;
column-gap: 20px;
margin-bottom: 40px;
.searchControl {
width: 500px;
}
}
.agentsContainer {
margin-top: 20px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 16px;
.agentItem {
display: flex;
width: 290px;
align-items: center;
border-radius: 10px;
border: 1px solid #e8e8e8;
padding: 12px;
cursor: pointer;
&:hover {
border-color: var(--chat-blue);
}
&.agentActive {
border-color: var(--chat-blue);
}
.agentIcon {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: var(--chat-blue);
width: 40px;
height: 40px;
border-radius: 50%;
background: #e8e8e8;
}
.agentContent {
margin-left: 12px;
flex: 1;
.agentNameBar {
display: flex;
align-items: center;
justify-content: space-between;
.agentName {
font-size: 14px;
font-weight: 500;
color: var(--text-color);
}
.operateIcons {
display: flex;
align-items: center;
column-gap: 12px;
.operateIcon {
color: var(--text-color-fourth);
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
}
}
.bottomBar {
display: flex;
align-items: center;
margin-top: 10px;
width: 210px;
.agentDescription {
width: 120px;
margin-right: 10px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 12px;
margin-top: 4px;
color: var(--text-color-third)
}
}
}
}
}
}
.toggleStatus {
display: flex;
align-items: center;
column-gap: 12px;
font-size: 12px;
.online {
color: var(--chat-blue);
}
}
.toolsSection {
.toolsSectionTitleBar {
padding: 20px 30px;
background-color: #fff;
display: flex;
align-items: center;
column-gap: 20px;
.backIcon {
font-size: 20px;
color: var(--text-color);
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
.agentTitle {
font-size: 24px;
font-weight: 700;
}
}
}
.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%;
}
}
}
.basicInfo {
margin: 20px;
background: #fff;
border-radius: 6px;
.basicInfoTitle {
display: flex;
align-items: center;
justify-content: space-between;
column-gap: 30px;
font-size: 16px;
font-weight: 500;
padding: 14px 20px;
border-bottom: 1px solid #e8e8e8;
}
.infoContent {
padding: 20px;
display: flex;
flex-direction: column;
row-gap: 12px;
}
}
.toolSection {
margin: 20px;
background: #fff;
border-radius: 6px;
.toolSectionTitleBar {
display: flex;
align-items: center;
justify-content: space-between;
column-gap: 30px;
padding: 14px 20px;
border-bottom: 1px solid #e8e8e8;
.toolSectionTitle {
font-size: 16px;
font-weight: 500;
}
}
.emptyHolder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
.toolsContent {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 20px 20px 30px;
.toolItem {
width: 300px;
padding: 12px;
border-radius: 10px;
border: 1px solid #e8e8e8;
display: flex;
align-items: center;
column-gap: 12px;
cursor: pointer;
.toolIcon {
font-size: 24px;
color: var(--chat-blue);
}
.toolContent {
flex: 1;
.toolTopSection {
display: flex;
align-items: center;
justify-content: space-between;
.toolType {
flex: 1;
color: var(--text-color);
font-weight: 500;
font-size: 16px;
}
.toolOperateIcons {
display: flex;
align-items: center;
column-gap: 10px;
.toolOperateIcon {
color: var(--text-color-third);
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
}
}
.toolDesc {
margin-top: 2px;
width: 220px;
font-size: 13px;
color: var(--text-color-third);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
}

View File

@@ -0,0 +1,119 @@
export type MetricOptionType = {
id: string;
metricId?: number;
modelId?: number;
}
export enum AgentToolTypeEnum {
RULE = 'RULE',
DSL = 'DSL',
PLUGIN = 'PLUGIN',
INTERPRET = 'INTERPRET'
}
export enum QueryModeEnum {
ENTITY_DETAIL = 'ENTITY_DETAIL',
ENTITY_LIST_FILTER = 'ENTITY_LIST_FILTER',
ENTITY_ID = 'ENTITY_ID',
METRIC_ENTITY = 'METRIC_ENTITY',
METRIC_FILTER = 'METRIC_FILTER',
METRIC_GROUPBY = 'METRIC_GROUPBY',
METRIC_MODEL = 'METRIC_MODEL',
METRIC_ORDERBY = 'METRIC_ORDERBY'
}
export const AGENT_TOOL_TYPE_LIST = [
{
label: '规则',
value: AgentToolTypeEnum.RULE
},
{
label: 'LLM语义解析',
value: AgentToolTypeEnum.DSL
},
{
label: '指标解读',
value: AgentToolTypeEnum.INTERPRET
},
{
label: '插件',
value: AgentToolTypeEnum.PLUGIN
},
]
export const QUERY_MODE_LIST = [
{
label: '实体明细(查询维度信息)',
value: QueryModeEnum.ENTITY_DETAIL
},
{
label: '实体圈选',
value: QueryModeEnum.ENTITY_LIST_FILTER
},
{
label: '实体查询(按ID查询)',
value: QueryModeEnum.ENTITY_ID
},
{
label: '指标查询(带实体)',
value: QueryModeEnum.METRIC_ENTITY
},
{
label: '指标查询(带条件)',
value: QueryModeEnum.METRIC_FILTER
},
{
label: '指标查询(按维度分组)',
value: QueryModeEnum.METRIC_GROUPBY
},
{
label: '指标查询(不带条件)',
value: QueryModeEnum.METRIC_MODEL
},
{
label: '按指标排序',
value: QueryModeEnum.METRIC_ORDERBY
}
];
export type AgentToolType = {
id?: string;
type: AgentToolTypeEnum;
name: string;
queryModes?: QueryModeEnum[];
plugins?: number[];
metricOptions?: MetricOptionType[];
exampleQuestions?: string[];
modelIds?: number[];
}
export type AgentConfigType = {
tools: AgentToolType[];
}
export type AgentType = {
id?: number;
name?: string;
description?: string;
createdBy?: string;
updatedBy?: string;
createdAt?: string;
updatedAt?: string;
examples?: string[];
status?: 0 | 1;
enableSearch?: 0 | 1;
agentConfig?: AgentConfigType;
}
export type ModelType = {
id: number | string;
parentId: number;
name: string;
bizName: string;
};
export type MetricType = {
id: number;
name: string;
bizName: string;
};

View File

@@ -8,24 +8,26 @@ import type { ForwardRefRenderFunction } from 'react';
import { searchRecommend } from 'supersonic-chat-sdk';
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
import styles from './style.less';
import { PLACE_HOLDER } from '../constants';
import { DefaultEntityType, ModelType } from '../type';
import { DefaultEntityType, AgentType, ModelType } from '../type';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
type Props = {
inputMsg: string;
chatId?: number;
currentModel?: ModelType;
currentAgent?: AgentType;
defaultEntity?: DefaultEntityType;
isCopilotMode?: boolean;
copilotFullscreen?: boolean;
models: ModelType[];
agentList: AgentType[];
collapsed: boolean;
onToggleCollapseBtn: () => void;
onInputMsgChange: (value: string) => void;
onSendMsg: (msg: string, modelId?: number) => void;
onAddConversation: () => void;
onCancelDefaultFilter: () => void;
onSelectAgent: (agent: AgentType) => void;
};
const { OptGroup, Option } = Select;
@@ -45,8 +47,10 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
inputMsg,
chatId,
currentModel,
currentAgent,
defaultEntity,
models,
agentList,
collapsed,
isCopilotMode,
copilotFullscreen,
@@ -55,10 +59,11 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
onSendMsg,
onAddConversation,
onCancelDefaultFilter,
onSelectAgent,
},
ref,
) => {
const [modelOptions, setModelOptions] = useState<ModelType[]>([]);
const [modelOptions, setModelOptions] = useState<(ModelType | AgentType)[]>([]);
const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({});
const [open, setOpen] = useState(false);
const [focused, setFocused] = useState(false);
@@ -121,6 +126,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
const model = models.find((item) => msg.includes(`@${item.name}`));
msgValue = model ? msg.replace(`@${model.name}`, '') : msg;
modelId = model?.id;
} else if (msg?.[0] === '/') {
const agent = agentList.find((item) => msg.includes(`/${item.name}`));
msgValue = agent ? msg.replace(`/${agent.name}`, '') : msg;
}
return { msgValue, modelId };
};
@@ -163,9 +171,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
useEffect(() => {
if (inputMsg.length === 1 && inputMsg[0] === '@') {
if (inputMsg.length === 1 && (inputMsg[0] === '@' || inputMsg[0] === '/')) {
setOpen(true);
setModelOptions(models);
setModelOptions(inputMsg[0] === '/' ? agentList : models);
setStepOptions({});
return;
} else {
@@ -173,10 +181,10 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
if (modelOptions.length > 0) {
setTimeout(() => {
setModelOptions([]);
}, 500);
}, 50);
}
}
if (!isSelect) {
if (!isSelect && currentAgent?.name !== '问知识') {
debounceGetWords(inputMsg, models, chatId, currentModel);
} else {
isSelect = false;
@@ -237,6 +245,12 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
isSelect = true;
if (modelOptions.length === 0) {
sendMsg(value);
} else {
const agent = agentList.find((item) => value.includes(item.name));
if (agent) {
onSelectAgent(agent);
onInputMsgChange('');
}
}
setOpen(false);
setTimeout(() => {
@@ -249,12 +263,98 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
[styles.defaultCopilotMode]: isCopilotMode && !copilotFullscreen,
});
const restrictNode = currentModel && !isMobile && (
<div className={styles.currentModel}>
<div className={styles.currentModelName}>
<span className={styles.quoteText}>
{!defaultEntity && <>{currentModel.name}</>}
{defaultEntity && (
<>
<span>{`${currentModel.name.slice(0, currentModel.name.length - 1)}`}</span>
<span className={styles.entityName} title={defaultEntity.entityName}>
{defaultEntity.entityName}
</span>
<span></span>
</>
)}
</span>
</div>
<div className={styles.cancelModel} onClick={onCancelDefaultFilter}>
</div>
</div>
);
const modelOptionNodes = modelOptions.map((model) => {
return (
<Option
key={model.id}
value={inputMsg[0] === '/' ? `/${model.name} ` : `@${model.name} `}
className={styles.searchOption}
>
{model.name}
</Option>
);
});
const associateOptionNodes = Object.keys(stepOptions).map((key) => {
return (
<OptGroup key={key} label={key}>
{stepOptions[key].map((option) => {
let optionValue =
Object.keys(stepOptions).length === 1
? option.recommend
: `${option.modelName || ''}${option.recommend}`;
if (inputMsg[0] === '@') {
const model = models.find((item) => inputMsg.includes(item.name));
optionValue = model ? `@${model.name} ${option.recommend}` : optionValue;
} else if (inputMsg[0] === '/') {
const agent = agentList.find((item) => inputMsg.includes(item.name));
optionValue = agent ? `/${agent.name} ${option.recommend}` : optionValue;
}
return (
<Option
key={`${option.recommend}${option.modelName ? `_${option.modelName}` : ''}`}
value={optionValue}
className={styles.searchOption}
>
<div className={styles.optionContent}>
{option.schemaElementType && (
<Tag
className={styles.semanticType}
color={
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
option.schemaElementType === SemanticTypeEnum.MODEL
? 'blue'
: option.schemaElementType === SemanticTypeEnum.VALUE
? 'geekblue'
: 'cyan'
}
>
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
option.schemaElementType ||
'维度'}
</Tag>
)}
{option.subRecommend}
</div>
</Option>
);
})}
</OptGroup>
);
});
return (
<div className={chatFooterClass}>
<div className={styles.composer}>
<div className={styles.collapseBtn} onClick={onToggleCollapseBtn}>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
{!isMobile && (
<div className={styles.collapseBtn} onClick={onToggleCollapseBtn}>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
)}
<Tooltip title="新建对话">
<IconFont
type="icon-icon-add-conversation-line"
@@ -263,36 +363,14 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
/>
</Tooltip>
<div className={styles.composerInputWrapper}>
{currentModel && (
<div className={styles.currentModel}>
<div className={styles.currentModelName}>
<span className={styles.quoteText}>
{currentModel.name}
{defaultEntity && (
<>
<span></span>
<span>{`${currentModel.name.slice(0, currentModel.name.length - 1)}`}</span>
<span className={styles.entityName} title={defaultEntity.entityName}>
{defaultEntity.entityName}
</span>
<span></span>
</>
)}
</span>
</div>
<div className={styles.cancelModel} onClick={onCancelDefaultFilter}>
</div>
</div>
)}
{/* {restrictNode}
{currentAgentNode} */}
<AutoComplete
className={styles.composerInput}
placeholder={
currentModel
? `请输入${currentModel.name}主题的问题,可使用@切换到其他主题`
: PLACE_HOLDER
currentAgent?.name
? `智能助理${currentAgent?.name}将与您对话,可输入“/”切换助理`
: '请输入您的问题'
}
value={inputMsg}
onChange={onInputMsgChange}
@@ -302,10 +380,20 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
ref={inputRef}
id="chatInput"
onKeyDown={(e) => {
if ((e.code === 'Enter' || e.code === 'NumpadEnter') && !isSelect) {
const chatInputEl: any = document.getElementById('chatInput');
sendMsg(chatInputEl.value);
setOpen(false);
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
{
const chatInputEl: any = document.getElementById('chatInput');
if (!isSelect) {
sendMsg(chatInputEl.value);
setOpen(false);
} else {
const agent = agentList.find((item) => chatInputEl.value.includes(item.name));
if (agent) {
onSelectAgent(agent);
onInputMsgChange('');
}
}
}
}
}}
onFocus={() => {
@@ -320,64 +408,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
open={open}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
>
{modelOptions.length > 0
? modelOptions.map((model) => {
return (
<Option
key={model.id}
value={`@${model.name} `}
className={styles.searchOption}
>
{model.name}
</Option>
);
})
: Object.keys(stepOptions).map((key) => {
return (
<OptGroup key={key} label={key}>
{stepOptions[key].map((option) => {
let optionValue =
Object.keys(stepOptions).length === 1
? option.recommend
: `${option.modelName || ''}${option.recommend}`;
if (inputMsg[0] === '@') {
const model = models.find((item) => inputMsg.includes(item.name));
optionValue = model ? `@${model.name} ${option.recommend}` : optionValue;
}
return (
<Option
key={`${option.recommend}${
option.modelName ? `_${option.modelName}` : ''
}`}
value={optionValue}
className={styles.searchOption}
>
<div className={styles.optionContent}>
{option.schemaElementType && (
<Tag
className={styles.semanticType}
color={
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
option.schemaElementType === SemanticTypeEnum.MODEL
? 'blue'
: option.schemaElementType === SemanticTypeEnum.VALUE
? 'geekblue'
: 'cyan'
}
>
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
option.schemaElementType ||
'维度'}
</Tag>
)}
{option.subRecommend}
</div>
</Option>
);
})}
</OptGroup>
);
})}
{modelOptions.length > 0 ? modelOptionNodes : associateOptionNodes}
</AutoComplete>
<div
className={classNames(styles.sendBtn, {

View File

@@ -90,7 +90,12 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
defaultEntityFilter?.entityName && window.location.pathname.includes('detail')
? defaultEntityFilter.entityName
: defaultModelName;
onAddConversation({ name: conversationName, type: 'CUSTOMIZE' });
onAddConversation({
name: conversationName,
type: 'CUSTOMIZE',
modelId: defaultEntityFilter?.modelId,
entityId: defaultEntityFilter?.entityId,
});
onNewConversationTriggered?.();
}
}, [triggerNewConversation]);

View File

@@ -3,10 +3,13 @@ import { memo, useCallback, useEffect, useState } from 'react';
import { isEqual } from 'lodash';
import { ChatItem } from 'supersonic-chat-sdk';
import type { MsgDataType } from 'supersonic-chat-sdk';
import { MessageItem, MessageTypeEnum } from './type';
import { AgentType, MessageItem, MessageTypeEnum } from './type';
import Plugin from './components/Plugin';
import { updateMessageContainerScroll } from '@/utils/utils';
import styles from './style.less';
import { MODEL_MODEL_ENTITY_ID_FILTER_MAP } from './constants';
import AgentList from './components/AgentList';
import RecommendQuestions from './components/RecommendQuestions';
type Props = {
id: string;
@@ -15,6 +18,7 @@ type Props = {
isMobileMode?: boolean;
conversationCollapsed: boolean;
copilotFullscreen?: boolean;
agentList: AgentType[];
onClickMessageContainer: () => void;
onMsgDataLoaded: (
data: MsgDataType,
@@ -24,6 +28,8 @@ type Props = {
) => void;
onCheckMore: (data: MsgDataType) => void;
onApplyAuth: (model: string) => void;
onSendMsg: (value: string) => void;
onSelectAgent: (agent: AgentType) => void;
};
const MessageContainer: React.FC<Props> = ({
@@ -33,10 +39,12 @@ const MessageContainer: React.FC<Props> = ({
isMobileMode,
conversationCollapsed,
copilotFullscreen,
agentList,
onClickMessageContainer,
onMsgDataLoaded,
onCheckMore,
onApplyAuth,
onSendMsg,
onSelectAgent,
}) => {
const [triggerResize, setTriggerResize] = useState(false);
@@ -97,6 +105,7 @@ const MessageContainer: React.FC<Props> = ({
}
return [
{
...MODEL_MODEL_ENTITY_ID_FILTER_MAP[modelId],
value: entityId,
},
];
@@ -109,6 +118,7 @@ const MessageContainer: React.FC<Props> = ({
const {
id: msgId,
modelId,
agentId,
entityId,
type,
msg,
@@ -125,6 +135,17 @@ const MessageContainer: React.FC<Props> = ({
return (
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
{type === MessageTypeEnum.RECOMMEND_QUESTIONS && (
<RecommendQuestions onSelectQuestion={onSendMsg} />
)}
{type === MessageTypeEnum.AGENT_LIST && (
<AgentList
currentAgentName={msg!}
data={agentList}
copilotFullscreen={copilotFullscreen}
onSelectAgent={onSelectAgent}
/>
)}
{type === MessageTypeEnum.QUESTION && (
<>
<Text position="right" data={msg} />
@@ -134,6 +155,7 @@ const MessageContainer: React.FC<Props> = ({
msgData={msgData}
conversationId={chatId}
modelId={modelId}
agentId={agentId}
filter={getFilters(modelId, entityId)}
isLastMessage={index === messageList.length - 1}
isMobileMode={isMobileMode}
@@ -150,6 +172,7 @@ const MessageContainer: React.FC<Props> = ({
msg={msgValue || msg || ''}
conversationId={chatId}
modelId={modelId}
agentId={agentId}
filter={getFilters(modelId, entityId)}
isLastMessage={index === messageList.length - 1}
isMobileMode={isMobileMode}
@@ -192,7 +215,8 @@ function areEqual(prevProps: Props, nextProps: Props) {
prevProps.id === nextProps.id &&
isEqual(prevProps.messageList, nextProps.messageList) &&
prevProps.conversationCollapsed === nextProps.conversationCollapsed &&
prevProps.copilotFullscreen === nextProps.copilotFullscreen
prevProps.copilotFullscreen === nextProps.copilotFullscreen &&
prevProps.agentList === nextProps.agentList
) {
return true;
}

View File

@@ -0,0 +1,63 @@
import LeftAvatar from '../CopilotAvatar';
import Message from '../Message';
import styles from './style.less';
import { AgentType } from '../../type';
import classNames from 'classnames';
type Props = {
currentAgentName: string;
data: AgentType[];
copilotFullscreen?: boolean;
onSelectAgent: (agent: AgentType) => void;
};
const AgentList: React.FC<Props> = ({
currentAgentName,
data,
copilotFullscreen,
onSelectAgent,
}) => {
const agentClass = classNames(styles.agent, {
[styles.fullscreen]: copilotFullscreen,
});
return (
<div className={styles.agentList}>
<LeftAvatar />
<Message position="left" bubbleClassName={styles.agentListMsg}>
<div className={styles.title}>
{currentAgentName}
/
</div>
<div className={styles.content}>
{data.map((agent) => (
<div
key={agent.id}
className={agentClass}
onClick={() => {
onSelectAgent(agent);
}}
>
<div className={styles.topBar}>
<div className={styles.agentName}>{agent.name}</div>
<div className={styles.tip}></div>
</div>
<div className={styles.examples}>
{agent.examples?.length > 0 ? (
agent.examples.map((example) => (
<div key={example} className={styles.example}>
{example}
</div>
))
) : (
<div className={styles.example}>{agent.description}</div>
)}
</div>
</div>
))}
</div>
</Message>
</div>
);
};
export default AgentList;

View File

@@ -0,0 +1,63 @@
.agentList {
display: flex;
margin-top: 12px;
.agentListMsg {
padding: 12px 20px 20px !important;
.title {
margin-bottom: 12px;
font-weight: 500;
font-size: 14px;
}
.content {
display: flex;
flex-wrap: wrap;
margin-top: 20px;
column-gap: 14px;
row-gap: 14px;
.agent {
flex: 0 0 calc(50% - 7px);
padding: 10px 14px 20px;
color: var(--text-color);
font-size: 14px;
background-color: #f4f4f4;
border-radius: 17px;
cursor: pointer;
.topBar {
.agentName {
font-weight: 700;
}
.tip {
margin-top: 2px;
font-size: 13px;
font-style: italic;
}
}
.examples {
display: flex;
flex-direction: column;
margin-top: 12px;
font-size: 13px;
row-gap: 8px;
.example {
padding: 4px 12px;
background-color: #ededed;
border-radius: 15px;
}
}
&.fullscreen {
flex: none;
width: 280px;
}
}
}
}
}

View File

@@ -9,8 +9,9 @@ import {
ModelType,
MessageItem,
MessageTypeEnum,
AgentType,
} from './type';
import { getModelList } from './service';
import { getModelList, queryAgentList } from './service';
import { useThrottleFn } from 'ahooks';
import Conversation from './Conversation';
import ChatFooter from './ChatFooter';
@@ -64,6 +65,8 @@ const Chat: React.FC<Props> = ({
const [applyAuthVisible, setApplyAuthVisible] = useState(false);
const [applyAuthModel, setApplyAuthModel] = useState('');
const [initialModelName, setInitialModelName] = useState('');
const [agentList, setAgentList] = useState<AgentType[]>([]);
const [currentAgent, setCurrentAgent] = useState<AgentType>();
const location = useLocation();
const dispatch = useDispatch();
const { modelName } = (location as any).query;
@@ -71,9 +74,19 @@ const Chat: React.FC<Props> = ({
const conversationRef = useRef<any>();
const chatFooterRef = useRef<any>();
const initAgentList = async () => {
const res = await queryAgentList();
const agentListValue = (res.data || []).filter((item) => item.status === 1);
setAgentList(agentListValue);
if (agentListValue.length > 0) {
setCurrentAgent(agentListValue[0]);
}
};
useEffect(() => {
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
initModels();
initAgentList();
}, []);
useEffect(() => {
@@ -102,7 +115,13 @@ const Chat: React.FC<Props> = ({
if (initMsg) {
inputFocus();
if (initMsg === 'CUSTOMIZE' && copilotSendMsg) {
onSendMsg(copilotSendMsg, [], modelId, entityId);
onSendMsg(
copilotSendMsg,
[],
modelId,
entityId,
agentList.find((item) => item.name === '做分析'),
);
dispatch({
type: 'globalState/setCopilotSendMsg',
payload: '',
@@ -143,16 +162,9 @@ const Chat: React.FC<Props> = ({
setMessageList([
{
id: uuid(),
type: MessageTypeEnum.TEXT,
msg: defaultModelName
? `您好,请输入关于${
defaultEntityFilter?.entityName
? `${defaultModelName?.slice(0, defaultModelName?.length - 1)}${
defaultEntityFilter?.entityName
}`
: `${defaultModelName}`
}的问题`
: '您好,请问有什么我能帮您吗?',
type: MessageTypeEnum.RECOMMEND_QUESTIONS,
// type: MessageTypeEnum.AGENT_LIST,
// msg: currentAgent?.name || '查信息',
},
]);
};
@@ -161,7 +173,6 @@ const Chat: React.FC<Props> = ({
return list.map((item: HistoryMsgItemType) => ({
id: item.questionId,
type:
item.queryResult?.queryMode === MessageTypeEnum.PLUGIN ||
item.queryResult?.queryMode === MessageTypeEnum.WEB_PAGE
? MessageTypeEnum.PLUGIN
: MessageTypeEnum.QUESTION,
@@ -212,6 +223,10 @@ const Chat: React.FC<Props> = ({
}
};
const changeAgent = (agent?: AgentType) => {
setCurrentAgent(agent);
};
const initModels = async () => {
const res = await getModelList();
const modelList = getLeafList(res.data);
@@ -236,6 +251,7 @@ const Chat: React.FC<Props> = ({
list?: MessageItem[],
modelId?: number,
entityId?: string,
agent?: AgentType,
) => {
const currentMsg = msg || inputMsg;
if (currentMsg.trim() === '') {
@@ -252,13 +268,26 @@ const Chat: React.FC<Props> = ({
modelChanged = currentModel?.id !== toModel?.id;
}
const modelIdValue = modelId || msgModel?.id || currentModel?.id;
const msgAgent = agentList.find((item) => currentMsg.indexOf(item.name) === 1);
const certainAgent = currentMsg[0] === '/' && msgAgent;
const agentIdValue = certainAgent ? msgAgent.id : undefined;
if (agent || certainAgent) {
changeAgent(agent || msgAgent);
}
const msgs = [
...(list || messageList),
{
id: uuid(),
msg: currentMsg,
msgValue: certainModel ? currentMsg.replace(`@${msgModel.name}`, '').trim() : currentMsg,
msgValue: certainModel
? currentMsg.replace(`@${msgModel.name}`, '').trim()
: certainAgent
? currentMsg.replace(`/${certainAgent.name}`, '').trim()
: currentMsg,
modelId: modelIdValue === -1 ? undefined : modelIdValue,
agentId: agent?.id || agentIdValue || currentAgent?.id,
entityId: entityId || (modelChanged ? undefined : defaultEntity?.entityId),
identityMsg: certainModel ? getIdentityMsgText(msgModel) : undefined,
type: MessageTypeEnum.QUESTION,
@@ -398,8 +427,22 @@ const Chat: React.FC<Props> = ({
inputFocus();
};
const onSelectAgent = (agent: AgentType) => {
setCurrentAgent(agent);
setMessageList([
...messageList,
{
id: uuid(),
type: MessageTypeEnum.TEXT,
msg: `您好,智能助理【${agent.name}】将与您对话,可输入“/”切换助理`,
},
]);
updateMessageContainerScroll();
};
const chatClass = classNames(styles.chat, {
[styles.mobile]: isMobileMode,
[styles.mobileMode]: isMobileMode,
[styles.mobile]: isMobile,
[styles.copilotFullscreen]: copilotFullscreen,
[styles.conversationCollapsed]: conversationCollapsed,
});
@@ -431,16 +474,21 @@ const Chat: React.FC<Props> = ({
isMobileMode={isMobileMode}
conversationCollapsed={conversationCollapsed}
copilotFullscreen={copilotFullscreen}
agentList={agentList}
onClickMessageContainer={inputFocus}
onMsgDataLoaded={onMsgDataLoaded}
onCheckMore={onCheckMore}
onApplyAuth={onApplyAuth}
onSendMsg={onSendMsg}
onSelectAgent={onSelectAgent}
/>
<ChatFooter
inputMsg={inputMsg}
chatId={currentConversation?.chatId}
models={models}
agentList={agentList}
currentModel={currentModel}
currentAgent={currentAgent}
defaultEntity={defaultEntity}
collapsed={conversationCollapsed}
isCopilotMode={isCopilotMode}
@@ -461,6 +509,7 @@ const Chat: React.FC<Props> = ({
onCancelCopilotFilter();
}
}}
onSelectAgent={onSelectAgent}
ref={chatFooterRef}
/>
</div>

View File

@@ -1,5 +1,5 @@
import { request } from 'umi';
import { ModelType } from './type';
import { AgentType, ModelType } from './type';
const prefix = '/api';
@@ -66,3 +66,9 @@ export function queryRecommendQuestions() {
method: 'GET',
});
}
export function queryAgentList() {
return request<Result<AgentType[]>>(`${prefix}/chat/agent/getAgentList`, {
method: 'GET',
});
}

View File

@@ -101,12 +101,12 @@
display: flex;
flex-direction: column;
padding: 20px 20px 60px 4px;
row-gap: 10px;
row-gap: 16px;
.messageItem {
display: flex;
flex-direction: column;
row-gap: 10px;
row-gap: 20px;
:global {
.ant-table-small {
@@ -240,9 +240,8 @@
}
}
&.mobile {
&.mobileMode {
height: 100% !important;
.chatSection {
width: 100% !important;
height: 100% !important;
@@ -276,6 +275,10 @@
}
}
}
&.mobile {
height: 100vh !important;
}
}
.conversation {
@@ -444,10 +447,6 @@
color: var(--primary-color);
}
// .messageItem {
// margin-top: 12px;
// }
.messageTime {
display: flex;
align-items: center;

View File

@@ -11,6 +11,7 @@ export enum MessageTypeEnum {
WEB_PAGE = 'WEB_PAGE', // 插件
RECOMMEND_QUESTIONS = 'recommend_questions', // 推荐问题
PARSE_OPTIONS = 'parse_options', // 解析选项
AGENT_LIST = 'agent_list', // 专家列表
}
export type MessageItem = {
@@ -20,6 +21,7 @@ export type MessageItem = {
msgValue?: string;
identityMsg?: string;
modelId?: number;
agentId?: number;
entityId?: string;
msgData?: MsgDataType;
quote?: string;
@@ -47,7 +49,6 @@ export enum MessageModeEnum {
export type ModelType = {
id: number;
parentId: number;
name: string;
bizName: string;
};
@@ -69,6 +70,7 @@ export type DefaultEntityType = {
entityId: string;
entityName: string;
modelName?: string;
modelId?: number;
};
export type SuggestionItemType = {
@@ -82,3 +84,11 @@ export type SuggestionType = {
dimensions: SuggestionItemType[];
metrics: SuggestionItemType[];
};
export type AgentType = {
id: number;
name: string;
description: string;
examples: string[];
status: 0 | 1;
};

View File

@@ -12,9 +12,9 @@ import {
} from './type';
import { getLeafList, uuid } from '@/utils/utils';
import styles from './style.less';
import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants';
import { PLUGIN_TYPE_MAP } from './constants';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { isArray, set } from 'lodash';
import { isArray } from 'lodash';
const FormItem = Form.Item;
const { TextArea } = Input;

View File

@@ -1,7 +1,8 @@
export const PLUGIN_TYPE_MAP = {
WEB_PAGE: '外链页面',
WEB_PAGE: 'Web页面',
WEB_SERVICE: 'Web服务',
DSL: 'LLM语义解析',
// DSL: 'LLM语义解析',
// CONTENT_INTERPRET: '内容解读',
}
export const PARSE_MODE_MAP = {