mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 19:51:00 +00:00
[feature](webapp) upgrade agent
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "supersonic-chat-sdk",
|
||||
"version": "0.4.32",
|
||||
"version": "0.5.28",
|
||||
"main": "dist/index.es.js",
|
||||
"module": "dist/index.es.js",
|
||||
"unpkg": "dist/index.umd.js",
|
||||
@@ -192,4 +192,4 @@
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,13 @@ export type ModelType = {
|
||||
useCnt: number;
|
||||
}
|
||||
|
||||
export type EntityDimensionType = {
|
||||
bizName: string;
|
||||
itemId: number;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ChatContextType = {
|
||||
id: number;
|
||||
queryId: number;
|
||||
@@ -73,6 +80,7 @@ export type ChatContextType = {
|
||||
dimensions: FieldType[];
|
||||
metrics: FieldType[];
|
||||
entity: { alias: string[], id: number };
|
||||
entityInfo: { dimensions: EntityDimensionType[] };
|
||||
elementMatches: any[];
|
||||
queryMode: string;
|
||||
dimensionFilters: FilterItemType[];
|
||||
@@ -137,6 +145,7 @@ export type ParseDataType = {
|
||||
}
|
||||
|
||||
export type QueryDataType = {
|
||||
chatContext: ChatContextType;
|
||||
aggregateInfo: AggregateInfoType;
|
||||
queryColumns: ColumnType[];
|
||||
queryResults: any[];
|
||||
|
||||
@@ -7,29 +7,23 @@ import WebPage from '../ChatMsg/WebPage';
|
||||
import Loading from './Loading';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
queryId?: number;
|
||||
executeLoading: boolean;
|
||||
entitySwitchLoading: boolean;
|
||||
chartIndex: number;
|
||||
executeTip?: string;
|
||||
data?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
onChangeChart: () => void;
|
||||
};
|
||||
|
||||
const ExecuteItem: React.FC<Props> = ({
|
||||
question,
|
||||
queryId,
|
||||
executeLoading,
|
||||
entitySwitchLoading,
|
||||
chartIndex,
|
||||
executeTip,
|
||||
data,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
onChangeChart,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
@@ -71,13 +65,7 @@ const ExecuteItem: React.FC<Props> = ({
|
||||
{data?.queryMode === 'WEB_PAGE' ? (
|
||||
<WebPage id={queryId!} data={data} />
|
||||
) : (
|
||||
<ChatMsg
|
||||
question={question}
|
||||
data={data}
|
||||
chartIndex={chartIndex}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
<ChatMsg data={data} chartIndex={chartIndex} triggerResize={triggerResize} />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Select, Spin } from 'antd';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { FilterItemType } from '../../common/type';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { queryDimensionValues } from '../../service';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
type Props = {
|
||||
modelId: number;
|
||||
filters: FilterItemType[];
|
||||
filter: FilterItemType;
|
||||
onFiltersChange: (filters: FilterItemType[]) => void;
|
||||
};
|
||||
|
||||
const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange }) => {
|
||||
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fetchRef = useRef(0);
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-filter-item`;
|
||||
|
||||
const initData = async () => {
|
||||
const { data } = await queryDimensionValues(modelId, filter.bizName, '');
|
||||
setOptions(
|
||||
data?.data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof filter.value === 'string' && options.length === 0) {
|
||||
initData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debounceFetcher = useMemo(() => {
|
||||
const loadOptions = (value: string) => {
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
setOptions([]);
|
||||
setLoading(true);
|
||||
|
||||
queryDimensionValues(modelId, filter.bizName, value).then(newOptions => {
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(
|
||||
newOptions.data?.data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return debounce(loadOptions, 800);
|
||||
}, [queryDimensionValues]);
|
||||
|
||||
const onChange = (value: string) => {
|
||||
const newFilters = filters.map(item => {
|
||||
if (item.bizName === filter.bizName) {
|
||||
item.value = `${value}`;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
onFiltersChange(newFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={prefixCls}>
|
||||
{typeof filter.value === 'string' ? (
|
||||
<Select
|
||||
bordered={false}
|
||||
value={filter.value}
|
||||
options={options}
|
||||
className={`${prefixCls}-select-control`}
|
||||
popupClassName={`${prefixCls}-select-popup`}
|
||||
onSearch={debounceFetcher}
|
||||
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||
onChange={onChange}
|
||||
showSearch
|
||||
/>
|
||||
) : (
|
||||
<span>{filter.value}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterItem;
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||
import { ChatContextType } from '../../common/type';
|
||||
import { ChatContextType, FilterItemType } from '../../common/type';
|
||||
import { CheckCircleFilled, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import SwicthEntity from './SwitchEntity';
|
||||
import { Tooltip } from 'antd';
|
||||
import Loading from './Loading';
|
||||
import FilterItem from './FilterItem';
|
||||
|
||||
type Props = {
|
||||
parseLoading: boolean;
|
||||
parseInfoOptions: ChatContextType[];
|
||||
parseTip: string;
|
||||
currentParseInfo?: ChatContextType;
|
||||
optionMode?: boolean;
|
||||
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
||||
onSwitchEntity: (entityId: string) => void;
|
||||
onFiltersChange: (filters: FilterItemType[]) => void;
|
||||
};
|
||||
|
||||
const MAX_OPTION_VALUES_COUNT = 2;
|
||||
@@ -24,9 +24,9 @@ const ParseTip: React.FC<Props> = ({
|
||||
parseInfoOptions,
|
||||
parseTip,
|
||||
currentParseInfo,
|
||||
optionMode,
|
||||
onSelectParseInfo,
|
||||
onSwitchEntity,
|
||||
onFiltersChange,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
@@ -62,6 +62,7 @@ const ParseTip: React.FC<Props> = ({
|
||||
|
||||
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||
const {
|
||||
modelId,
|
||||
modelName,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
@@ -73,27 +74,12 @@ const ParseTip: React.FC<Props> = ({
|
||||
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 itemValueClass = `${prefixCls}-tip-item-value`;
|
||||
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||
@@ -106,14 +92,22 @@ const ParseTip: React.FC<Props> = ({
|
||||
const getFilterContent = (filters: any) => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||
{filters.map((filter: any, index: number) => (
|
||||
<div className={itemValueClass}>
|
||||
{filters.map((filter: any) => (
|
||||
<div className={`${prefixCls}-tip-item-option`}>
|
||||
<span>
|
||||
{filter.name}
|
||||
<span className={`${prefixCls}-tip-item-filter-name`}>{filter.name}</span>
|
||||
{filter.operator !== '=' ? ` ${filter.operator} ` : ':'}
|
||||
</span>
|
||||
<span>{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}</span>
|
||||
{index !== filters.length - 1 && <span>、</span>}
|
||||
{queryMode !== 'DSL' && !filter.bizName?.includes('_id') ? (
|
||||
<FilterItem
|
||||
modelId={modelId}
|
||||
filters={dimensionFilters}
|
||||
filter={filter}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
) : (
|
||||
<span className={itemValueClass}>{filter.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -124,27 +118,16 @@ const ParseTip: React.FC<Props> = ({
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||
<Tooltip
|
||||
title={
|
||||
dimensionFilters.length > MAX_OPTION_VALUES_COUNT
|
||||
? getFilterContent(dimensionFilters)
|
||||
: ''
|
||||
}
|
||||
color="#fff"
|
||||
overlayStyle={{ maxWidth: 'none' }}
|
||||
>
|
||||
<div className={`${prefixCls}-tip-item-content`}>
|
||||
{getFilterContent(dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT))}
|
||||
{dimensionFilters.length > MAX_OPTION_VALUES_COUNT && ' ...'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className={`${prefixCls}-tip-item-content`}>
|
||||
{getFilterContent(dimensionFilters)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tipContentClass}
|
||||
className={`${prefixCls}-tip-content`}
|
||||
onClick={() => {
|
||||
if (isOptions && currentParseInfo === undefined) {
|
||||
onSelectParseInfo(parseInfo);
|
||||
@@ -234,41 +217,45 @@ const ParseTip: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
let tipNode: ReactNode;
|
||||
const parseInfo = parseInfoOptions[0] || {};
|
||||
const { properties, entity, entityInfo, elementMatches, queryMode } = parseInfo || {};
|
||||
|
||||
if (parseInfoOptions.length > 1 || optionMode) {
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-multi-options`}>
|
||||
<div>
|
||||
还有以下的相关问题,<strong>请您点击提交</strong>
|
||||
</div>
|
||||
<div className={`${prefixCls}-options`}>
|
||||
{parseInfoOptions.map((item, index) => getTipNode(item, true, index))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const { type } = parseInfoOptions[0]?.properties || {};
|
||||
const entityAlias = parseInfoOptions[0]?.entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = parseInfoOptions[0]?.elementMatches?.find(
|
||||
item => item.element?.type === 'ID'
|
||||
)?.element.name;
|
||||
const queryMode = parseInfoOptions[0]?.queryMode;
|
||||
const { type } = properties || {};
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
{getTipNode(parseInfoOptions[0])}
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
<InfoCircleOutlined />
|
||||
<div>
|
||||
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||
const entityDimensions = entityInfo?.dimensions?.filter(
|
||||
item =>
|
||||
!['zyqk_song_id', 'song_name', 'singer_id'].includes(item.bizName) &&
|
||||
!(
|
||||
entityInfo?.dimensions?.some(dimension => dimension.bizName === 'singer_id') &&
|
||||
item.bizName === 'singer_name'
|
||||
)
|
||||
);
|
||||
|
||||
const tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
{getTipNode(parseInfo)}
|
||||
{queryMode !== 'ENTITY_ID' && entityDimensions?.length > 0 && (
|
||||
<div className={`${prefixCls}-entity-info`}>
|
||||
{entityDimensions.map(dimension => (
|
||||
<div className={`${prefixCls}-dimension-item`} key={dimension.itemId}>
|
||||
<div className={`${prefixCls}-dimension-name`}>{dimension.name}:</div>
|
||||
<div className={`${prefixCls}-dimension-value`}>{dimension.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
<InfoCircleOutlined />
|
||||
<div>
|
||||
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return getNode('意图解析结果', tipNode, true);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChatContextType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import { ChatContextType, FilterItemType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { chatExecute, chatParse, switchEntity } from '../../service';
|
||||
import { chatExecute, chatParse, queryData, switchEntity } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||
import IconFont from '../IconFont';
|
||||
import ParseTip from './ParseTip';
|
||||
@@ -17,7 +17,6 @@ type Props = {
|
||||
filter?: any[];
|
||||
isLastMessage?: boolean;
|
||||
msgData?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
isHistory?: boolean;
|
||||
triggerResize?: boolean;
|
||||
parseOptions?: ChatContextType[];
|
||||
@@ -32,7 +31,6 @@ const ChatItem: React.FC<Props> = ({
|
||||
agentId,
|
||||
filter,
|
||||
isLastMessage,
|
||||
isMobileMode,
|
||||
isHistory,
|
||||
triggerResize,
|
||||
msgData,
|
||||
@@ -77,10 +75,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
return true;
|
||||
};
|
||||
|
||||
const onExecute = async (
|
||||
parseInfoValue: ChatContextType,
|
||||
parseInfoOptions?: ChatContextType[]
|
||||
) => {
|
||||
const onExecute = async (parseInfoValue: ChatContextType) => {
|
||||
setExecuteMode(true);
|
||||
setExecuteLoading(true);
|
||||
try {
|
||||
@@ -88,24 +83,10 @@ const ChatItem: React.FC<Props> = ({
|
||||
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
|
||||
);
|
||||
@@ -141,7 +122,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
setParseInfoOptions(parseInfos || []);
|
||||
const parseInfoValue = parseInfos[0];
|
||||
setParseInfo(parseInfoValue);
|
||||
onExecute(parseInfoValue, parseInfos);
|
||||
onExecute(parseInfoValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -167,8 +148,15 @@ const ChatItem: React.FC<Props> = ({
|
||||
setParseInfoOptions([chatContext]);
|
||||
};
|
||||
|
||||
const onChangeChart = () => {
|
||||
setChartIndex(chartIndex + 1);
|
||||
const onFiltersChange = async (dimensionFilters: FilterItemType[]) => {
|
||||
setEntitySwitchLoading(true);
|
||||
const chatContextValue = { ...(parseInfoOptions[0] || {}), dimensionFilters };
|
||||
const res: any = await queryData(chatContextValue);
|
||||
setEntitySwitchLoading(false);
|
||||
const resChatContext = res.data?.data?.chatContext;
|
||||
setData({ ...(res.data?.data || {}), chatContext: resChatContext || chatContextValue });
|
||||
setParseInfo(resChatContext || chatContextValue);
|
||||
setParseInfoOptions([resChatContext || chatContextValue]);
|
||||
};
|
||||
|
||||
const onSelectParseInfo = async (parseInfoValue: ChatContextType) => {
|
||||
@@ -197,22 +185,19 @@ const ChatItem: React.FC<Props> = ({
|
||||
parseInfoOptions={parseOptions || parseInfoOptions.slice(0, 1)}
|
||||
parseTip={parseTip}
|
||||
currentParseInfo={parseInfo}
|
||||
optionMode={parseOptions !== undefined}
|
||||
onSelectParseInfo={onSelectParseInfo}
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
{executeMode && (
|
||||
<ExecuteItem
|
||||
question={msg}
|
||||
queryId={parseInfo?.queryId}
|
||||
executeLoading={executeLoading}
|
||||
entitySwitchLoading={entitySwitchLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
onChangeChart={onChangeChart}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import '../../styles/index.less';
|
||||
|
||||
@chat-item-prefix-cls: ~'@{supersonic-chat-prefix}-item';
|
||||
@filter-item-prefix-cls: ~'@{supersonic-chat-prefix}-filter-item';
|
||||
|
||||
.@{chat-item-prefix-cls} {
|
||||
display: flex;
|
||||
@@ -215,6 +216,12 @@
|
||||
&-tip-item-filter-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
&-tip-item-filter-name {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
&-mode-name {
|
||||
@@ -231,6 +238,29 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-entity-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-dimension-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-dimension-name {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-dimension-value {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-metric-info-list {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
@@ -263,3 +293,14 @@
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.@{filter-item-prefix-cls} {
|
||||
&-select-control {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import { formatByDecimalPlaces, formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import ApplyAuth from '../ApplyAuth';
|
||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||
import PeriodCompareItem from './PeriodCompareItem';
|
||||
@@ -27,12 +27,13 @@ const MetricCard: React.FC<Props> = ({
|
||||
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const { dateInfo } = chatContext || {};
|
||||
const { startDate } = dateInfo || {};
|
||||
|
||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
||||
|
||||
const { dataFormatType, dataFormat } = indicatorColumn || {};
|
||||
const value = queryResults?.[0]?.[indicatorColumnName] || 0;
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||
|
||||
const matricCardClass = classNames(prefixCls, {
|
||||
@@ -54,7 +55,7 @@ const MetricCard: React.FC<Props> = ({
|
||||
{indicatorColumn?.name ? (
|
||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||
) : (
|
||||
<div style={{ height: 6 }} />
|
||||
<div style={{ height: 10 }} />
|
||||
)}
|
||||
{drillDownDimension && (
|
||||
<div className={`${prefixCls}-filter-section-wrapper`}>
|
||||
@@ -73,19 +74,25 @@ const MetricCard: React.FC<Props> = ({
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
<div className={indicatorClass}>
|
||||
<div className={`${prefixCls}-date-range`}>{startDate}</div>
|
||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||
<ApplyAuth model={entityInfo?.modelInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{isNumber
|
||||
? formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'
|
||||
: formatNumberWithCN(+queryResults?.[0]?.[indicatorColumnName])}
|
||||
</div>
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
{dataFormatType === 'percent' || dataFormatType === 'decimal'
|
||||
? `${formatByDecimalPlaces(
|
||||
dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||
dataFormat?.decimalPlaces || 2
|
||||
)}${dataFormatType === 'percent' ? '%' : ''}`
|
||||
: isNumber
|
||||
? formatMetric(value) || '-'
|
||||
: formatNumberWithCN(+value)}
|
||||
</div>
|
||||
{!isNaN(+value) && +value >= 10000 && (
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{metricInfos?.length > 0 && (
|
||||
@@ -100,8 +107,8 @@ const MetricCard: React.FC<Props> = ({
|
||||
{queryMode.includes('METRIC') && (
|
||||
<div className={`${prefixCls}-drill-down-dimensions`}>
|
||||
<DrillDownDimensions
|
||||
modelId={chatContext.modelId}
|
||||
dimensionFilters={chatContext.dimensionFilters}
|
||||
modelId={chatContext?.modelId}
|
||||
dimensionFilters={chatContext?.dimensionFilters}
|
||||
drillDownDimension={drillDownDimension}
|
||||
onSelectDimension={onSelectDimension}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
.@{metric-card-prefix-cls} {
|
||||
width: 100%;
|
||||
height: 162px;
|
||||
row-gap: 4px;
|
||||
|
||||
&-dsl {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import { AggregateInfoType } from '../../../common/type';
|
||||
import { formatByDecimalPlaces, formatMetric, formatNumberWithCN } from '../../../utils/utils';
|
||||
import { AggregateInfoType, ColumnType } from '../../../common/type';
|
||||
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||
import { SwapOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
aggregateInfo: AggregateInfoType;
|
||||
currentMetricField: ColumnType;
|
||||
};
|
||||
|
||||
const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||
const MetricInfo: React.FC<Props> = ({ aggregateInfo, currentMetricField }) => {
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const metricInfo = metricInfos?.[0] || {};
|
||||
const { date, value, statistics } = metricInfo || {};
|
||||
const { dataFormatType, dataFormat } = currentMetricField;
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-metric-info`;
|
||||
|
||||
@@ -26,11 +28,20 @@ const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||
<div className={`${prefixCls}-indicator`}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{isNumber ? formatMetric(value) : formatNumberWithCN(+value)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
{dataFormatType === 'percent' || dataFormatType === 'decimal'
|
||||
? `${formatByDecimalPlaces(
|
||||
dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||
dataFormat?.decimalPlaces || 2
|
||||
)}${dataFormatType === 'percent' ? '%' : ''}`
|
||||
: isNumber
|
||||
? formatMetric(value)
|
||||
: formatNumberWithCN(+value)}
|
||||
</div>
|
||||
{!isNaN(+value) && +value >= 10000 && (
|
||||
<div className={`${prefixCls}-indicator-switch`}>
|
||||
<SwapOutlined onClick={handleNumberClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-bottom-section`}>
|
||||
<div className={`${prefixCls}-date`}>
|
||||
|
||||
@@ -147,11 +147,12 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
}</span><span style="display: inline-block; width: 90px; text-align: right; font-weight: 500;">${
|
||||
item.value === ''
|
||||
? '-'
|
||||
: metricField.dataFormatType === 'percent'
|
||||
: metricField.dataFormatType === 'percent' ||
|
||||
metricField.dataFormatType === 'decimal'
|
||||
? `${formatByDecimalPlaces(
|
||||
item.value,
|
||||
metricField.dataFormat?.decimalPlaces || 2
|
||||
)}%`
|
||||
)}${metricField.dataFormatType === 'percent' ? '%' : ''}`
|
||||
: getFormattedValue(item.value)
|
||||
}</span></div>`
|
||||
)
|
||||
@@ -176,7 +177,8 @@ const MetricTrendChart: React.FC<Props> = ({
|
||||
smooth: true,
|
||||
data: data.map((item: any) => {
|
||||
const value = item[valueColumnName];
|
||||
return metricField.dataFormatType === 'percent' &&
|
||||
return (metricField.dataFormatType === 'percent' ||
|
||||
metricField.dataFormatType === 'decimal') &&
|
||||
metricField.dataFormat?.needMultiply100
|
||||
? value * 100
|
||||
: value;
|
||||
|
||||
@@ -177,7 +177,10 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
|
||||
<Spin spinning={loading}>
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{!isMobile && aggregateInfoValue?.metricInfos?.length > 0 && (
|
||||
<MetricInfo aggregateInfo={aggregateInfoValue} />
|
||||
<MetricInfo
|
||||
aggregateInfo={aggregateInfoValue}
|
||||
currentMetricField={currentMetricField}
|
||||
/>
|
||||
)}
|
||||
<div className={`${prefixCls}-date-options`}>
|
||||
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
row-gap: 4px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
&-metric-fields {
|
||||
@@ -198,7 +198,7 @@
|
||||
&-indicator-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 36px;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
margin-top: 2px;
|
||||
color: var(--text-color-secondary);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import Bar from './Bar';
|
||||
import MetricCard from './MetricCard';
|
||||
import MetricTrend from './MetricTrend';
|
||||
@@ -10,14 +9,12 @@ import classNames from 'classnames';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
data: MsgDataType;
|
||||
chartIndex: number;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
};
|
||||
|
||||
const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, triggerResize }) => {
|
||||
const ChatMsg: React.FC<Props> = ({ data, chartIndex, triggerResize }) => {
|
||||
const { queryColumns, queryResults, chatContext, queryMode } = data;
|
||||
|
||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||
@@ -169,19 +166,6 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
|
||||
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
||||
};
|
||||
|
||||
// let width = '100%';
|
||||
// if (isText) {
|
||||
// width = 'fit-content';
|
||||
// } else if (isMetricCard) {
|
||||
// width = isDslMetricCard ? '290px' : '370px';
|
||||
// } else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||
// if (columns.length === 1) {
|
||||
// width = '600px';
|
||||
// } else if (columns.length === 2) {
|
||||
// width = '1000px';
|
||||
// }
|
||||
// }
|
||||
|
||||
const chartMsgClass = classNames({ [prefixCls]: !isTable });
|
||||
|
||||
return <div className={chartMsgClass}>{getMsgContent()}</div>;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
flex-wrap: wrap;
|
||||
column-gap: 6px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-metric-card {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_qiku6b2kol.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_sz2crkuyuj.js',
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -61,7 +61,6 @@ const Chat = () => {
|
||||
agentId={6}
|
||||
onMsgDataLoaded={onMsgDataLoaded}
|
||||
isLastMessage
|
||||
isMobileMode
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -55,8 +55,8 @@ export function switchEntity(entityId: string, modelId?: number, chatId?: number
|
||||
});
|
||||
}
|
||||
|
||||
export function queryData(chatContext: ChatContextType) {
|
||||
return axios.post<Result<QueryDataType>>(`${prefix}/chat/query/queryData`, chatContext);
|
||||
export function queryData(chatContext: Partial<ChatContextType>) {
|
||||
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/queryData`, chatContext);
|
||||
}
|
||||
|
||||
export function queryContext(queryText: string, chatId?: number) {
|
||||
@@ -95,3 +95,7 @@ export function updateQAFeedback(questionId: number, score: number) {
|
||||
export function queryDrillDownDimensions(modelId: number) {
|
||||
return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${modelId}`);
|
||||
}
|
||||
|
||||
export function queryDimensionValues(modelId: number, bizName: string, value: string) {
|
||||
return axios.post<Result<any>>(`${prefix}/chat/query/queryDimensionValue`, { modelId, bizName, value});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export const getFormattedValue = (value: number | string, remainZero?: boolean)
|
||||
+value >= 100000000
|
||||
? NumericUnit.OneHundredMillion
|
||||
: +value >= 10000
|
||||
? NumericUnit.EnTenThousand
|
||||
? NumericUnit.TenThousand
|
||||
: NumericUnit.None;
|
||||
|
||||
let formattedValue = formatByUnit(value, unit);
|
||||
@@ -127,7 +127,7 @@ export const normalizeTrendData = (
|
||||
) => {
|
||||
const dateList = enumerateDaysBetweenDates(moment(startDate), moment(endDate), dateType);
|
||||
const result = dateList.map((date) => {
|
||||
const item = resultList.find((result) => result[dateColumnName] === date);
|
||||
const item = resultList.find((result) => moment(result[dateColumnName]).format(dateType === 'months' ? 'YYYY-MM' : 'YYYY-MM-DD') === date);
|
||||
return {
|
||||
...(item || {}),
|
||||
[dateColumnName]: date,
|
||||
@@ -161,8 +161,9 @@ export function getLightenDarkenColor(col, amt) {
|
||||
} else {
|
||||
result = hexToRgbObj(col) || {};
|
||||
}
|
||||
return `rgba(${result.r + amt},${result.g + amt},${result.b + amt}${result.a ? `,${result.a}` : ''
|
||||
})`;
|
||||
return `rgba(${result.r + amt},${result.g + amt},${result.b + amt}${
|
||||
result.a ? `,${result.a}` : ''
|
||||
})`;
|
||||
}
|
||||
|
||||
export function getChartLightenColor(col) {
|
||||
|
||||
@@ -4,19 +4,18 @@ const Settings: LayoutSettings & {
|
||||
pwa?: boolean;
|
||||
logo?: string;
|
||||
} = {
|
||||
navTheme: 'light',
|
||||
navTheme: 'dark',
|
||||
primaryColor: '#296DF3',
|
||||
layout: 'mix',
|
||||
layout: 'top',
|
||||
contentWidth: 'Fluid',
|
||||
fixedHeader: false,
|
||||
fixSiderbar: true,
|
||||
colorWeak: false,
|
||||
title: '',
|
||||
pwa: false,
|
||||
iconfontUrl: '//at.alicdn.com/t/c/font_4120566_qiku6b2kol.js',
|
||||
iconfontUrl: '//at.alicdn.com/t/c/font_4120566_x5c4www9bqm.js',
|
||||
splitMenus: true,
|
||||
menu: {
|
||||
defaultOpenAll: true,
|
||||
autoClose: false,
|
||||
ignoreFlatMenu: true,
|
||||
},
|
||||
|
||||
@@ -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.4.32",
|
||||
"supersonic-chat-sdk": "^0.5.28",
|
||||
"umi": "3.5",
|
||||
"umi-request": "^1.0.8"
|
||||
},
|
||||
@@ -145,4 +145,4 @@
|
||||
"@types/react": "17.0.0"
|
||||
},
|
||||
"__npminstall_done": false
|
||||
}
|
||||
}
|
||||
@@ -158,13 +158,14 @@ export const layout: RunTimeLayoutConfig = (params) => {
|
||||
menuHeaderRender: undefined,
|
||||
childrenRender: (dom) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{ height: location.pathname.includes('chat') ? 'calc(100vh - 48px)' : undefined }}
|
||||
>
|
||||
{dom}
|
||||
{history.location.pathname !== '/chat' && !isMobile && <Copilot />}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
openKeys: false,
|
||||
...initialState?.settings,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,11 +7,14 @@ export default {
|
||||
'menu.exception.not-permission': '403',
|
||||
'menu.exception.not-find': '404',
|
||||
'menu.exception.server-error': '500',
|
||||
'menu.semanticModel': '模型管理',
|
||||
'menu.semanticModel': '语义模型',
|
||||
'menu.semanticModel.model': '模型管理',
|
||||
'menu.semanticModel.database': '数据库连接',
|
||||
'menu.metric': '指标市场',
|
||||
'menu.database': '数据库连接',
|
||||
'menu.chatSetting': '问答设置',
|
||||
'menu.chatPlugin': '问答插件',
|
||||
'menu.chatPlugin': '第三方插件',
|
||||
'menu.login': '登录',
|
||||
'menu.chat': '问答对话',
|
||||
'menu.agent': '问答助理'
|
||||
'menu.agent': '智能助理',
|
||||
};
|
||||
|
||||
@@ -116,50 +116,50 @@ const ToolModal: React.FC<Props> = ({ editTool, onSaveTool, onCancel }) => {
|
||||
<FormItem name="name" label="名称">
|
||||
<Input placeholder="请输入工具名称" />
|
||||
</FormItem>
|
||||
{(toolType === AgentToolTypeEnum.RULE || toolType === AgentToolTypeEnum.DSL) && (
|
||||
<FormItem name="modelIds" label="主题域">
|
||||
<Select
|
||||
options={modelList.map((model) => ({ label: model.name, value: model.id }))}
|
||||
placeholder="请选择主题域"
|
||||
mode="multiple"
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
<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 && (
|
||||
<>
|
||||
|
||||
@@ -24,19 +24,19 @@ export enum QueryModeEnum {
|
||||
|
||||
export const AGENT_TOOL_TYPE_LIST = [
|
||||
{
|
||||
label: '规则',
|
||||
label: '规则语义解析',
|
||||
value: AgentToolTypeEnum.RULE
|
||||
},
|
||||
{
|
||||
label: 'LLM语义解析',
|
||||
label: '大模型语义解析',
|
||||
value: AgentToolTypeEnum.DSL
|
||||
},
|
||||
{
|
||||
label: '指标解读',
|
||||
label: '大模型指标解读',
|
||||
value: AgentToolTypeEnum.INTERPRET
|
||||
},
|
||||
{
|
||||
label: '插件',
|
||||
label: '第三方插件',
|
||||
value: AgentToolTypeEnum.PLUGIN
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { AgentType } from '../type';
|
||||
import styles from './style.less';
|
||||
import classNames from 'classnames';
|
||||
import { message } from 'antd';
|
||||
import IconFont from '@/components/IconFont';
|
||||
|
||||
const agents = [
|
||||
'icon-fukuanbaobiaochaxun',
|
||||
'icon-hangweifenxi1',
|
||||
'icon-xiaofeifenxi',
|
||||
'icon-renwuchaxun',
|
||||
'icon-liushuichaxun',
|
||||
'icon-baobiao',
|
||||
'icon-cangkuchaxun',
|
||||
'icon-xiaoshoushuju',
|
||||
'icon-tongji',
|
||||
'icon-shujutongji',
|
||||
'icon-mendiankanban',
|
||||
];
|
||||
|
||||
type Props = {
|
||||
agentList: AgentType[];
|
||||
currentAgent?: AgentType;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
};
|
||||
|
||||
const AgentList: React.FC<Props> = ({ agentList, currentAgent, onSelectAgent }) => {
|
||||
const onAddAgent = () => {
|
||||
message.info('正在开发中,敬请期待');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.agentList}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerTitle}>智能助理</div>
|
||||
<PlusCircleOutlined className={styles.plusIcon} onClick={onAddAgent} />
|
||||
</div>
|
||||
<div className={styles.agentListContent}>
|
||||
{agentList.map((agent, index) => {
|
||||
const agentItemClass = classNames(styles.agentItem, {
|
||||
[styles.active]: currentAgent?.id === agent.id,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={agentItemClass}
|
||||
onClick={() => {
|
||||
onSelectAgent(agent);
|
||||
}}
|
||||
>
|
||||
{/* <img src={agents[index % agents.length]} alt="avatar" className={styles.avatar} /> */}
|
||||
<IconFont type={agents[index % agents.length]} className={styles.avatar} />
|
||||
<div className={styles.agentInfo}>
|
||||
<div className={styles.agentName}>{agent.name}</div>
|
||||
<div className={styles.agentDesc}>{agent.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentList;
|
||||
@@ -0,0 +1,78 @@
|
||||
.agentList {
|
||||
width: 248px;
|
||||
height: 100%;
|
||||
background: #f9f9f9;
|
||||
border-right: 1px solid #f1f1f1;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 16px;
|
||||
|
||||
.headerTitle {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.plusIcon {
|
||||
color: var(--text-color);
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agentListContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 8px;
|
||||
row-gap: 2px;
|
||||
|
||||
.agentItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 4px;
|
||||
column-gap: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
// width: 40px;
|
||||
// height: 40px;
|
||||
// border-radius: 8px;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.agentInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 2px;
|
||||
|
||||
.agentName {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agentDesc {
|
||||
width: 160px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { getTextWidth, groupByColumn, isMobile } from '@/utils/utils';
|
||||
import { AutoComplete, Select, Tag, Tooltip } from 'antd';
|
||||
import { AutoComplete, Select, Tag } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
@@ -8,25 +8,17 @@ import type { ForwardRefRenderFunction } from 'react';
|
||||
import { searchRecommend } from 'supersonic-chat-sdk';
|
||||
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
|
||||
import styles from './style.less';
|
||||
import { DefaultEntityType, AgentType, ModelType } from '../type';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||
import { AgentType, ModelType } from '../type';
|
||||
|
||||
type Props = {
|
||||
inputMsg: string;
|
||||
chatId?: number;
|
||||
currentModel?: ModelType;
|
||||
currentAgent?: AgentType;
|
||||
defaultEntity?: DefaultEntityType;
|
||||
isCopilotMode?: boolean;
|
||||
copilotFullscreen?: boolean;
|
||||
models: ModelType[];
|
||||
agentList: AgentType[];
|
||||
collapsed: boolean;
|
||||
onToggleCollapseBtn: () => void;
|
||||
onToggleHistoryVisible: () => void;
|
||||
onInputMsgChange: (value: string) => void;
|
||||
onSendMsg: (msg: string, modelId?: number) => void;
|
||||
onAddConversation: () => void;
|
||||
onCancelDefaultFilter: () => void;
|
||||
onAddConversation: (agent?: AgentType) => void;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
};
|
||||
|
||||
@@ -46,19 +38,12 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
{
|
||||
inputMsg,
|
||||
chatId,
|
||||
currentModel,
|
||||
currentAgent,
|
||||
defaultEntity,
|
||||
models,
|
||||
agentList,
|
||||
collapsed,
|
||||
isCopilotMode,
|
||||
copilotFullscreen,
|
||||
onToggleCollapseBtn,
|
||||
onToggleHistoryVisible,
|
||||
onInputMsgChange,
|
||||
onSendMsg,
|
||||
onAddConversation,
|
||||
onCancelDefaultFilter,
|
||||
onSelectAgent,
|
||||
},
|
||||
ref,
|
||||
@@ -119,14 +104,10 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
: data;
|
||||
};
|
||||
|
||||
const processMsg = (msg: string, models: ModelType[]) => {
|
||||
const processMsg = (msg: string) => {
|
||||
let msgValue = msg;
|
||||
let modelId: number | undefined;
|
||||
if (msg?.[0] === '@') {
|
||||
const model = models.find((item) => msg.includes(`@${item.name}`));
|
||||
msgValue = model ? msg.replace(`@${model.name}`, '') : msg;
|
||||
modelId = model?.id;
|
||||
} else if (msg?.[0] === '/') {
|
||||
if (msg?.[0] === '/') {
|
||||
const agent = agentList.find((item) => msg.includes(`/${item.name}`));
|
||||
msgValue = agent ? msg.replace(`/${agent.name}`, '') : msg;
|
||||
}
|
||||
@@ -134,12 +115,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
};
|
||||
|
||||
const debounceGetWordsFunc = useCallback(() => {
|
||||
const getAssociateWords = async (
|
||||
msg: string,
|
||||
models: ModelType[],
|
||||
chatId?: number,
|
||||
model?: ModelType,
|
||||
) => {
|
||||
const getAssociateWords = async (msg: string, chatId?: number, currentAgent?: AgentType) => {
|
||||
if (isPinyin) {
|
||||
return;
|
||||
}
|
||||
@@ -148,9 +124,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
}
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
const { msgValue, modelId } = processMsg(msg, models);
|
||||
const modelIdValue = modelId || model?.id;
|
||||
const res = await searchRecommend(msgValue.trim(), chatId, modelIdValue);
|
||||
const { msgValue, modelId } = processMsg(msg);
|
||||
const res = await searchRecommend(msgValue.trim(), chatId, modelId, currentAgent?.id);
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -171,9 +146,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputMsg.length === 1 && (inputMsg[0] === '@' || inputMsg[0] === '/')) {
|
||||
if (inputMsg.length === 1 && inputMsg[0] === '/') {
|
||||
setOpen(true);
|
||||
setModelOptions(inputMsg[0] === '/' ? agentList : models);
|
||||
setModelOptions(agentList);
|
||||
setStepOptions({});
|
||||
return;
|
||||
} else {
|
||||
@@ -184,8 +159,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
if (!isSelect && currentAgent?.name !== '问知识') {
|
||||
debounceGetWords(inputMsg, models, chatId, currentModel);
|
||||
if (!isSelect) {
|
||||
debounceGetWords(inputMsg, chatId, currentAgent);
|
||||
} else {
|
||||
isSelect = false;
|
||||
}
|
||||
@@ -248,7 +223,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
} else {
|
||||
const agent = agentList.find((item) => value.includes(item.name));
|
||||
if (agent) {
|
||||
onSelectAgent(agent);
|
||||
if (agent.id !== currentAgent?.id) {
|
||||
onSelectAgent(agent);
|
||||
}
|
||||
onInputMsgChange('');
|
||||
}
|
||||
}
|
||||
@@ -260,40 +237,11 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
|
||||
const chatFooterClass = classNames(styles.chatFooter, {
|
||||
[styles.mobile]: isMobile,
|
||||
[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}
|
||||
>
|
||||
<Option key={model.id} value={`/${model.name} `} className={styles.searchOption}>
|
||||
{model.name}
|
||||
</Option>
|
||||
);
|
||||
@@ -307,10 +255,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
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] === '/') {
|
||||
if (inputMsg[0] === '/') {
|
||||
const agent = agentList.find((item) => inputMsg.includes(item.name));
|
||||
optionValue = agent ? `/${agent.name} ${option.recommend}` : optionValue;
|
||||
}
|
||||
@@ -349,31 +294,34 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
|
||||
return (
|
||||
<div className={chatFooterClass}>
|
||||
<div className={styles.composer}>
|
||||
<div className={styles.tools}>
|
||||
<div
|
||||
className={styles.toolItem}
|
||||
onClick={() => {
|
||||
onAddConversation();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-c003xiaoxiduihua" className={styles.toolIcon} />
|
||||
<div>新对话</div>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className={styles.collapseBtn} onClick={onToggleCollapseBtn}>
|
||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
<div className={styles.toolItem} onClick={onToggleHistoryVisible}>
|
||||
<IconFont type="icon-lishi" className={styles.toolIcon} />
|
||||
<div>历史对话</div>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip title="新建对话">
|
||||
<IconFont
|
||||
type="icon-icon-add-conversation-line"
|
||||
className={styles.addConversation}
|
||||
onClick={onAddConversation}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.composer}>
|
||||
<div className={styles.composerInputWrapper}>
|
||||
{/* {restrictNode}
|
||||
{currentAgentNode} */}
|
||||
<AutoComplete
|
||||
className={styles.composerInput}
|
||||
placeholder={
|
||||
currentAgent?.name
|
||||
? `智能助理【${currentAgent?.name}】将与您对话,可输入“/”切换助理`
|
||||
: '请输入您的问题'
|
||||
}
|
||||
placeholder={`智能助理${
|
||||
isMobile ? `[${currentAgent?.name}]` : `【${currentAgent?.name}】`
|
||||
}将与您对话,输入“/”可切换助理`}
|
||||
value={inputMsg}
|
||||
onChange={onInputMsgChange}
|
||||
onChange={(value: string) => {
|
||||
onInputMsgChange(value);
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
autoFocus={!isMobile}
|
||||
backfill
|
||||
@@ -389,7 +337,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
} else {
|
||||
const agent = agentList.find((item) => chatInputEl.value.includes(item.name));
|
||||
if (agent) {
|
||||
onSelectAgent(agent);
|
||||
if (agent.id !== currentAgent?.id) {
|
||||
onSelectAgent(agent);
|
||||
}
|
||||
onInputMsgChange('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,34 @@
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 6px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 40px;
|
||||
margin: 6px 20px 20px;
|
||||
|
||||
&.defaultCopilotMode {
|
||||
margin-bottom: 30px;
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
column-gap: 8px;
|
||||
|
||||
.toolItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 12px;
|
||||
column-gap: 6px;
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
height: 46px;
|
||||
height: 70px;
|
||||
|
||||
.collapseBtn {
|
||||
height: 46px;
|
||||
@@ -45,50 +62,6 @@
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.currentModel {
|
||||
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;
|
||||
|
||||
.currentModelName {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
|
||||
.entityName {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelModel {
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -98,18 +71,17 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
background: #fff;
|
||||
background: #f9f9f9;
|
||||
border: 0;
|
||||
border-radius: 24px;
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px -0.5px 0px, rgba(0, 0, 0, 0.1) 0px 0px 18px;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
resize: none;
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
height: 100% !important;
|
||||
padding: 0 20px;
|
||||
height: 40px !important;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.ant-select-selection-search {
|
||||
@@ -118,8 +90,8 @@
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
padding-left: 10px !important;
|
||||
line-height: 45px;
|
||||
padding-left: 6px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +118,8 @@
|
||||
|
||||
.sendBtn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -158,7 +130,6 @@
|
||||
background-color: rgb(184, 184, 191);
|
||||
border: unset;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: background-color 0.3s ease 0s;
|
||||
|
||||
&.sendBtnActive {
|
||||
@@ -167,9 +138,7 @@
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
height: 40px;
|
||||
margin: 12px;
|
||||
margin-bottom: 20px;
|
||||
margin: 6px 10px 10px;
|
||||
|
||||
.addConversation {
|
||||
height: 40px;
|
||||
@@ -178,17 +147,26 @@
|
||||
|
||||
.composer {
|
||||
height: 40px;
|
||||
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
line-height: 39px !important;
|
||||
padding-left: 0 !important;
|
||||
line-height: 38px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
right: 4px;
|
||||
bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { Dropdown, Input, Menu } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
@@ -9,19 +8,20 @@ import {
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useLocation } from 'umi';
|
||||
import ConversationModal from './components/ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from './service';
|
||||
import ConversationModal from '../components/ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from '../service';
|
||||
import styles from './style.less';
|
||||
import { ConversationDetailType, DefaultEntityType } from './type';
|
||||
import { DEFAULT_CONVERSATION_NAME } from './constants';
|
||||
import { AgentType, ConversationDetailType, DefaultEntityType } from '../type';
|
||||
import { DEFAULT_CONVERSATION_NAME } from '../constants';
|
||||
import moment from 'moment';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { CloseOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
|
||||
type Props = {
|
||||
agentList?: AgentType[];
|
||||
currentAgent?: AgentType;
|
||||
currentConversation?: ConversationDetailType;
|
||||
collapsed?: boolean;
|
||||
historyVisible?: boolean;
|
||||
isCopilotMode?: boolean;
|
||||
defaultModelName?: string;
|
||||
defaultEntityFilter?: DefaultEntityType;
|
||||
triggerNewConversation?: boolean;
|
||||
onNewConversationTriggered?: () => void;
|
||||
@@ -30,19 +30,23 @@ type Props = {
|
||||
name?: string,
|
||||
modelId?: number,
|
||||
entityId?: string,
|
||||
agent?: AgentType,
|
||||
) => void;
|
||||
onCloseConversation: () => void;
|
||||
};
|
||||
|
||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
{
|
||||
agentList,
|
||||
currentAgent,
|
||||
currentConversation,
|
||||
collapsed,
|
||||
historyVisible,
|
||||
isCopilotMode,
|
||||
defaultModelName,
|
||||
defaultEntityFilter,
|
||||
triggerNewConversation,
|
||||
onNewConversationTriggered,
|
||||
onSelectConversation,
|
||||
onCloseConversation,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -58,24 +62,20 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
onAddConversation,
|
||||
}));
|
||||
|
||||
const updateData = async () => {
|
||||
const { data } = await getAllConversations();
|
||||
const updateData = async (agent?: AgentType) => {
|
||||
const { data } = await getAllConversations(agent?.id || currentAgent?.id);
|
||||
const conversationList = data || [];
|
||||
setConversations(conversationList.slice(0, 500));
|
||||
setConversations(conversationList.slice(0, 200));
|
||||
return conversationList;
|
||||
};
|
||||
|
||||
const initData = async () => {
|
||||
const data = await updateData();
|
||||
if (data.length > 0) {
|
||||
const chatId = localStorage.getItem('CONVERSATION_ID') || cid;
|
||||
const chatId = cid;
|
||||
if (chatId) {
|
||||
const conversation = data.find((item: any) => item.chatId === +chatId);
|
||||
if (conversation) {
|
||||
onSelectConversation(conversation);
|
||||
} else {
|
||||
onSelectConversation(data[0]);
|
||||
}
|
||||
onSelectConversation(conversation || data[0]);
|
||||
} else {
|
||||
onSelectConversation(data[0]);
|
||||
}
|
||||
@@ -84,36 +84,30 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerNewConversation) {
|
||||
const conversationName =
|
||||
defaultEntityFilter?.entityName && window.location.pathname.includes('detail')
|
||||
? defaultEntityFilter.entityName
|
||||
: defaultModelName;
|
||||
onAddConversation({
|
||||
name: conversationName,
|
||||
type: 'CUSTOMIZE',
|
||||
modelId: defaultEntityFilter?.modelId,
|
||||
entityId: defaultEntityFilter?.entityId,
|
||||
});
|
||||
onNewConversationTriggered?.();
|
||||
}
|
||||
}, [triggerNewConversation]);
|
||||
// useEffect(() => {
|
||||
// if (triggerNewConversation) {
|
||||
// return;
|
||||
// }
|
||||
// if (q && cid === undefined && window.location.href.includes('/chat')) {
|
||||
// onAddConversation({
|
||||
// name: q,
|
||||
// modelId: modelId ? +modelId : undefined,
|
||||
// entityId,
|
||||
// });
|
||||
// } else {
|
||||
// initData();
|
||||
// }
|
||||
// }, [q]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerNewConversation) {
|
||||
return;
|
||||
}
|
||||
if (q && cid === undefined && window.location.href.includes('/workbench/chat')) {
|
||||
onAddConversation({ name: q, modelId: modelId ? +modelId : undefined, entityId });
|
||||
} else {
|
||||
if (currentAgent && !triggerNewConversation) {
|
||||
initData();
|
||||
}
|
||||
}, [q]);
|
||||
}, [currentAgent]);
|
||||
|
||||
const addConversation = async (name?: string) => {
|
||||
await saveConversation(name || DEFAULT_CONVERSATION_NAME);
|
||||
return updateData();
|
||||
const addConversation = async (name?: string, agent?: AgentType) => {
|
||||
await saveConversation(name || DEFAULT_CONVERSATION_NAME, agent?.id || currentAgent!.id);
|
||||
return updateData(agent);
|
||||
};
|
||||
|
||||
const onDeleteConversation = async (id: number) => {
|
||||
@@ -126,14 +120,24 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
modelId,
|
||||
entityId,
|
||||
type,
|
||||
agent,
|
||||
}: {
|
||||
name?: string;
|
||||
modelId?: number;
|
||||
entityId?: string;
|
||||
type?: string;
|
||||
agent?: AgentType;
|
||||
} = {}) => {
|
||||
const data = await addConversation(name);
|
||||
onSelectConversation(data[0], type || name, modelId, entityId);
|
||||
const data = await addConversation(name, agent);
|
||||
if (data.length > 0) {
|
||||
onSelectConversation(
|
||||
data[0],
|
||||
type || name || DEFAULT_CONVERSATION_NAME,
|
||||
modelId,
|
||||
entityId,
|
||||
agent,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onOperate = (key: string, conversation: ConversationDetailType) => {
|
||||
@@ -146,7 +150,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
};
|
||||
|
||||
const conversationClass = classNames(styles.conversation, {
|
||||
[styles.collapsed]: collapsed,
|
||||
[styles.historyVisible]: historyVisible,
|
||||
[styles.copilotMode]: isCopilotMode,
|
||||
});
|
||||
|
||||
@@ -171,7 +175,21 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
|
||||
return (
|
||||
<div className={conversationClass}>
|
||||
<div className={styles.leftSection}>
|
||||
<div className={styles.rightSection}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={styles.title}>历史对话</div>
|
||||
<div className={styles.rightOperation}>
|
||||
<div
|
||||
className={styles.newConversation}
|
||||
onClick={() => {
|
||||
addConversation();
|
||||
}}
|
||||
>
|
||||
新对话
|
||||
</div>
|
||||
<CloseOutlined className={styles.closeIcon} onClick={onCloseConversation} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.searchConversation}>
|
||||
<Input
|
||||
placeholder="搜索"
|
||||
@@ -215,15 +233,28 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
onSelectConversation(item);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-chat1" className={styles.conversationIcon} />
|
||||
<div className={styles.conversationContent}>
|
||||
<div className={styles.topTitleBar}>
|
||||
<div className={styles.conversationName}>{item.chatName}</div>
|
||||
<div className={styles.conversationTitleBar}>
|
||||
<div className={styles.conversationName}>{item.chatName}</div>
|
||||
{currentConversation?.chatId === item.chatId && (
|
||||
<div className={styles.currentConversation}>当前对话</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.conversationTime}>
|
||||
{convertTime(item.lastTime || '')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.subTitle}>{item.lastQuestion}</div>
|
||||
<div className={styles.bottomSection}>
|
||||
<div className={styles.subTitle}>{item.lastQuestion}</div>
|
||||
<DeleteOutlined
|
||||
className={styles.deleteIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteConversation(item.chatId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
@@ -0,0 +1,176 @@
|
||||
.conversation {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
|
||||
.rightSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.titleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.rightOperation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
|
||||
.newConversation {
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 70px);
|
||||
padding: 2px 0 0;
|
||||
overflow-y: auto;
|
||||
row-gap: 12px;
|
||||
|
||||
.conversationItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.conversationContent {
|
||||
width: 100%;
|
||||
|
||||
.topTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversationTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
|
||||
.conversationName {
|
||||
max-width: 300px;
|
||||
margin-right: 2px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.currentConversation {
|
||||
padding: 0 4px;
|
||||
color: var(--chat-blue);
|
||||
font-size: 12px;
|
||||
background-color: var(--light-blue-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.conversationTime {
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
|
||||
.subTitle {
|
||||
width: 350px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.deleteIcon {
|
||||
color: var(--text-color-six);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.activeConversationItem {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.historyVisible {
|
||||
width: 400px;
|
||||
padding: 10px 16px;
|
||||
border-left: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
&.copilotMode {
|
||||
&.collapsed {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,41 @@
|
||||
import Text from './components/Text';
|
||||
import Text from '../components/Text';
|
||||
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 { AgentType, MessageItem, MessageTypeEnum } from './type';
|
||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||
import { AgentType, MessageItem, MessageTypeEnum } from '../type';
|
||||
import { isMobile, 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';
|
||||
import { MODEL_MODEL_ENTITY_ID_FILTER_MAP } from '../constants';
|
||||
import AgentTip from '../components/AgentTip';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
chatId: number;
|
||||
messageList: MessageItem[];
|
||||
isMobileMode?: boolean;
|
||||
conversationCollapsed: boolean;
|
||||
copilotFullscreen?: boolean;
|
||||
agentList: AgentType[];
|
||||
onClickMessageContainer: () => void;
|
||||
historyVisible: boolean;
|
||||
currentAgent?: AgentType;
|
||||
chatVisible?: boolean;
|
||||
onMsgDataLoaded: (
|
||||
data: MsgDataType,
|
||||
questionId: string | number,
|
||||
question: string,
|
||||
valid: boolean,
|
||||
) => void;
|
||||
onCheckMore: (data: MsgDataType) => void;
|
||||
onApplyAuth: (model: string) => void;
|
||||
onSendMsg: (value: string) => void;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
};
|
||||
|
||||
const MessageContainer: React.FC<Props> = ({
|
||||
id,
|
||||
chatId,
|
||||
messageList,
|
||||
isMobileMode,
|
||||
conversationCollapsed,
|
||||
copilotFullscreen,
|
||||
agentList,
|
||||
onClickMessageContainer,
|
||||
historyVisible,
|
||||
currentAgent,
|
||||
chatVisible,
|
||||
onMsgDataLoaded,
|
||||
onCheckMore,
|
||||
onSendMsg,
|
||||
onSelectAgent,
|
||||
}) => {
|
||||
const [triggerResize, setTriggerResize] = useState(false);
|
||||
|
||||
@@ -63,12 +55,7 @@ const MessageContainer: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
}, [conversationCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
updateMessageContainerScroll();
|
||||
}, [copilotFullscreen]);
|
||||
}, [historyVisible, chatVisible]);
|
||||
|
||||
const getFilters = (modelId?: number, entityId?: string) => {
|
||||
if (!modelId || !entityId) {
|
||||
@@ -82,8 +69,10 @@ const MessageContainer: React.FC<Props> = ({
|
||||
];
|
||||
};
|
||||
|
||||
const messageContainerClass = classNames(styles.messageContainer, { [styles.mobile]: isMobile });
|
||||
|
||||
return (
|
||||
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
||||
<div id={id} className={messageContainerClass}>
|
||||
<div className={styles.messageList}>
|
||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||
const {
|
||||
@@ -104,16 +93,8 @@ 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 || !isMobileMode}
|
||||
onSelectAgent={onSelectAgent}
|
||||
/>
|
||||
<AgentTip currentAgent={currentAgent} onSendMsg={onSendMsg} />
|
||||
)}
|
||||
{type === MessageTypeEnum.QUESTION && (
|
||||
<>
|
||||
@@ -127,7 +108,6 @@ const MessageContainer: React.FC<Props> = ({
|
||||
agentId={agentId}
|
||||
filter={getFilters(modelId, entityId)}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
isMobileMode={isMobileMode}
|
||||
isHistory={isHistory}
|
||||
triggerResize={triggerResize}
|
||||
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||
@@ -145,7 +125,6 @@ const MessageContainer: React.FC<Props> = ({
|
||||
agentId={agentId}
|
||||
filter={getFilters(modelId, entityId)}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
isMobileMode={isMobileMode}
|
||||
isHistory={isHistory}
|
||||
triggerResize={triggerResize}
|
||||
parseOptions={parseOptions}
|
||||
@@ -167,9 +146,9 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
||||
if (
|
||||
prevProps.id === nextProps.id &&
|
||||
isEqual(prevProps.messageList, nextProps.messageList) &&
|
||||
prevProps.conversationCollapsed === nextProps.conversationCollapsed &&
|
||||
prevProps.copilotFullscreen === nextProps.copilotFullscreen &&
|
||||
prevProps.agentList === nextProps.agentList
|
||||
prevProps.historyVisible === nextProps.historyVisible &&
|
||||
prevProps.currentAgent === nextProps.currentAgent &&
|
||||
prevProps.chatVisible === nextProps.chatVisible
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
.messageContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
.messageList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 70px 20px 60px 14px;
|
||||
row-gap: 16px;
|
||||
|
||||
.messageItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
|
||||
: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 {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.ss-chat-table-even-row {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 16px 0 0;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-prev,
|
||||
.ant-pagination .ant-pagination-next {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
.ant-pagination-item-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-jump-prev,
|
||||
.ant-pagination-jump-next {
|
||||
.ant-pagination-item-link {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-options {
|
||||
display: inline-block;
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item-active {
|
||||
font-weight: 600;
|
||||
background-color: #ffffff;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5715;
|
||||
list-style: none;
|
||||
font-feature-settings: 'tnum', 'tnum';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.messageList {
|
||||
padding: 20px 10px 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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;
|
||||
@@ -1,63 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import LeftAvatar from '../CopilotAvatar';
|
||||
import Message from '../Message';
|
||||
import styles from './style.less';
|
||||
import { AgentType } from '../../type';
|
||||
import { isMobile } from '@/utils/utils';
|
||||
|
||||
type Props = {
|
||||
currentAgent?: AgentType;
|
||||
onSendMsg: (value: string) => void;
|
||||
};
|
||||
|
||||
const AgentTip: React.FC<Props> = ({ currentAgent, onSendMsg }) => {
|
||||
if (!currentAgent) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.agentTip}>
|
||||
{!isMobile && <LeftAvatar />}
|
||||
<Message position="left" bubbleClassName={styles.agentTipMsg}>
|
||||
<div className={styles.title}>
|
||||
您好,智能助理【{currentAgent.name}
|
||||
】将与您对话,试着问:
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.examples}>
|
||||
{currentAgent.examples?.length > 0 ? (
|
||||
currentAgent.examples.map((example) => (
|
||||
<div
|
||||
key={example}
|
||||
className={styles.example}
|
||||
onClick={() => {
|
||||
onSendMsg(example);
|
||||
}}
|
||||
>
|
||||
“{example}”
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.example}>{currentAgent.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Message>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentTip;
|
||||
@@ -0,0 +1,44 @@
|
||||
.agentTip {
|
||||
display: flex;
|
||||
|
||||
.agentTipMsg {
|
||||
padding: 12px 20px 20px !important;
|
||||
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
column-gap: 14px;
|
||||
|
||||
.topBar {
|
||||
.tip {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.examples {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
row-gap: 8px;
|
||||
|
||||
.example {
|
||||
color: var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
flex: none;
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
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';
|
||||
@@ -1,187 +0,0 @@
|
||||
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}>
|
||||
<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;
|
||||
@@ -1,69 +0,0 @@
|
||||
.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,6 +1,6 @@
|
||||
import { updateMessageContainerScroll, isMobile, uuid, getLeafList } from '@/utils/utils';
|
||||
import { updateMessageContainerScroll, isMobile, uuid } from '@/utils/utils';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Helmet, useDispatch, useLocation } from 'umi';
|
||||
import { Helmet, useDispatch } from 'umi';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import styles from './style.less';
|
||||
import {
|
||||
@@ -11,42 +11,38 @@ import {
|
||||
MessageTypeEnum,
|
||||
AgentType,
|
||||
} from './type';
|
||||
import { getModelList, queryAgentList } from './service';
|
||||
import { queryAgentList } from './service';
|
||||
import { useThrottleFn } from 'ahooks';
|
||||
import Conversation from './Conversation';
|
||||
import ChatFooter from './ChatFooter';
|
||||
import classNames from 'classnames';
|
||||
import { CHAT_TITLE, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from './constants';
|
||||
import { CHAT_TITLE, DEFAULT_CONVERSATION_NAME, MOBILE_TITLE, WEB_TITLE } from './constants';
|
||||
import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-sdk';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import 'supersonic-chat-sdk/dist/index.css';
|
||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||
import AgentList from './AgentList';
|
||||
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
||||
|
||||
type Props = {
|
||||
isCopilotMode?: boolean;
|
||||
copilotFullscreen?: boolean;
|
||||
defaultModelName?: string;
|
||||
defaultEntityFilter?: DefaultEntityType;
|
||||
copilotSendMsg?: string;
|
||||
triggerNewConversation?: boolean;
|
||||
chatVisible?: boolean;
|
||||
onNewConversationTriggered?: () => void;
|
||||
onCurrentModelChange?: (model?: ModelType) => void;
|
||||
onCancelCopilotFilter?: () => void;
|
||||
onCheckMoreDetail?: () => void;
|
||||
};
|
||||
|
||||
const Chat: React.FC<Props> = ({
|
||||
isCopilotMode,
|
||||
copilotFullscreen,
|
||||
defaultModelName,
|
||||
defaultEntityFilter,
|
||||
copilotSendMsg,
|
||||
triggerNewConversation,
|
||||
chatVisible,
|
||||
onNewConversationTriggered,
|
||||
onCurrentModelChange,
|
||||
onCancelCopilotFilter,
|
||||
onCheckMoreDetail,
|
||||
}) => {
|
||||
const isMobileMode = isMobile || isCopilotMode;
|
||||
|
||||
@@ -58,18 +54,11 @@ const Chat: React.FC<Props> = ({
|
||||
const [currentConversation, setCurrentConversation] = useState<
|
||||
ConversationDetailType | undefined
|
||||
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
|
||||
const [conversationCollapsed, setConversationCollapsed] = useState(isMobileMode);
|
||||
const [models, setModels] = useState<ModelType[]>([]);
|
||||
const [currentModel, setCurrentModel] = useState<ModelType>();
|
||||
const [historyVisible, setHistoryVisible] = useState(false);
|
||||
const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
|
||||
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;
|
||||
|
||||
const conversationRef = useRef<any>();
|
||||
const chatFooterRef = useRef<any>();
|
||||
@@ -85,33 +74,35 @@ const Chat: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
|
||||
initModels();
|
||||
initAgentList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (models.length > 0 && initialModelName && !currentModel) {
|
||||
changeModel(models.find((model) => model.name === initialModelName));
|
||||
if (chatVisible) {
|
||||
updateMessageContainerScroll();
|
||||
}
|
||||
}, [models]);
|
||||
}, [chatVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modelName) {
|
||||
setInitialModelName(modelName);
|
||||
if (triggerNewConversation) {
|
||||
setCurrentAgent(agentList?.find((item) => item.name === '做分析'));
|
||||
conversationRef.current?.onAddConversation({
|
||||
type: 'CUSTOMIZE',
|
||||
modelId: defaultEntityFilter?.modelId,
|
||||
entityId: defaultEntityFilter?.entityId,
|
||||
agent: agentList?.find((item) => item.name === '做分析'),
|
||||
});
|
||||
setTimeout(() => {
|
||||
onNewConversationTriggered?.();
|
||||
}, 200);
|
||||
}
|
||||
}, [modelName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModelName !== undefined && models.length > 0) {
|
||||
changeModel(models.find((model) => model.name === defaultModelName));
|
||||
}
|
||||
}, [defaultModelName]);
|
||||
}, [triggerNewConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentConversation) {
|
||||
return;
|
||||
}
|
||||
const { initMsg, modelId, entityId } = currentConversation;
|
||||
const { initMsg, modelId, entityId, agent } = currentConversation;
|
||||
if (initMsg) {
|
||||
inputFocus();
|
||||
if (initMsg === 'CUSTOMIZE' && copilotSendMsg) {
|
||||
@@ -129,10 +120,19 @@ const Chat: React.FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
if (initMsg === DEFAULT_CONVERSATION_NAME || initMsg.includes('CUSTOMIZE')) {
|
||||
sendHelloRsp();
|
||||
if (agent) {
|
||||
setCurrentAgent(agent);
|
||||
}
|
||||
sendHelloRsp(agent);
|
||||
return;
|
||||
}
|
||||
onSendMsg(initMsg, [], modelId, entityId);
|
||||
onSendMsg(
|
||||
initMsg,
|
||||
[],
|
||||
modelId,
|
||||
entityId,
|
||||
initMsg.includes('商业线索') ? agentList.find((item) => item.name === '做分析') : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
updateHistoryMsg(1);
|
||||
@@ -154,17 +154,12 @@ const Chat: React.FC<Props> = ({
|
||||
};
|
||||
}, [historyInited]);
|
||||
|
||||
useEffect(() => {
|
||||
inputFocus();
|
||||
}, [copilotFullscreen]);
|
||||
|
||||
const sendHelloRsp = () => {
|
||||
const sendHelloRsp = (agent?: AgentType) => {
|
||||
setMessageList([
|
||||
{
|
||||
// id: uuid(),
|
||||
// type: MessageTypeEnum.RECOMMEND_QUESTIONS,
|
||||
id: uuid(),
|
||||
type: MessageTypeEnum.AGENT_LIST,
|
||||
msg: currentAgent?.name || '查信息',
|
||||
msg: agent?.name || currentAgent?.name || '查信息',
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -213,26 +208,6 @@ const Chat: React.FC<Props> = ({
|
||||
},
|
||||
);
|
||||
|
||||
const changeModel = (model?: ModelType) => {
|
||||
setCurrentModel(model);
|
||||
if (onCurrentModelChange) {
|
||||
onCurrentModelChange(model);
|
||||
}
|
||||
};
|
||||
|
||||
const changeAgent = (agent?: AgentType) => {
|
||||
setCurrentAgent(agent);
|
||||
};
|
||||
|
||||
const initModels = async () => {
|
||||
const res = await getModelList();
|
||||
const modelList = getLeafList(res.data);
|
||||
setModels([{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...modelList].slice(0, 11));
|
||||
if (defaultModelName !== undefined) {
|
||||
changeModel(modelList.find((model) => model.name === defaultModelName));
|
||||
}
|
||||
};
|
||||
|
||||
const inputFocus = () => {
|
||||
if (!isMobile) {
|
||||
chatFooterRef.current?.inputFocus();
|
||||
@@ -255,22 +230,12 @@ const Chat: React.FC<Props> = ({
|
||||
setInputMsg('');
|
||||
return;
|
||||
}
|
||||
const msgModel = models.find((item) => currentMsg.includes(item.name));
|
||||
const certainModel = currentMsg[0] === '@' && msgModel;
|
||||
let modelChanged = false;
|
||||
|
||||
if (certainModel) {
|
||||
const toModel = msgModel.id === -1 ? undefined : msgModel;
|
||||
changeModel(toModel);
|
||||
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);
|
||||
setCurrentAgent(agent || msgAgent);
|
||||
}
|
||||
|
||||
const msgs = [
|
||||
@@ -278,15 +243,12 @@ const Chat: React.FC<Props> = ({
|
||||
{
|
||||
id: uuid(),
|
||||
msg: currentMsg,
|
||||
msgValue: certainModel
|
||||
? currentMsg.replace(`@${msgModel.name}`, '').trim()
|
||||
: certainAgent
|
||||
msgValue: certainAgent
|
||||
? currentMsg.replace(`/${certainAgent.name}`, '').trim()
|
||||
: currentMsg,
|
||||
modelId: modelIdValue === -1 ? undefined : modelIdValue,
|
||||
modelId: modelId === -1 ? undefined : modelId,
|
||||
agentId: agent?.id || agentIdValue || currentAgent?.id,
|
||||
entityId: entityId || (modelChanged ? undefined : defaultEntity?.entityId),
|
||||
identityMsg: certainModel ? getIdentityMsgText(msgModel) : undefined,
|
||||
entityId: entityId || defaultEntity?.entityId,
|
||||
type: MessageTypeEnum.QUESTION,
|
||||
},
|
||||
];
|
||||
@@ -315,6 +277,7 @@ const Chat: React.FC<Props> = ({
|
||||
name?: string,
|
||||
modelId?: number,
|
||||
entityId?: string,
|
||||
agent?: AgentType,
|
||||
) => {
|
||||
if (!isMobileMode) {
|
||||
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
|
||||
@@ -324,28 +287,12 @@ const Chat: React.FC<Props> = ({
|
||||
initMsg: name,
|
||||
modelId,
|
||||
entityId,
|
||||
agent,
|
||||
});
|
||||
saveConversationToLocal(conversation);
|
||||
};
|
||||
|
||||
const updateChatFilter = (data: MsgDataType) => {
|
||||
const { queryMode, dimensionFilters, elementMatches, modelName, model } = data.chatContext;
|
||||
if (queryMode !== 'ENTITY_LIST_FILTER') {
|
||||
return;
|
||||
}
|
||||
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
|
||||
const entityName = elementMatches?.find((item: any) => item.element?.type === 'ID')?.element
|
||||
?.name;
|
||||
|
||||
if (typeof entityId === 'string' && entityName) {
|
||||
setCurrentModel(model);
|
||||
setDefaultEntity({
|
||||
entityId,
|
||||
entityName,
|
||||
modelName,
|
||||
});
|
||||
}
|
||||
};
|
||||
const reportMsgEvent = (msg: string, valid: boolean) => {};
|
||||
|
||||
const onMsgDataLoaded = (
|
||||
data: MsgDataType,
|
||||
@@ -353,6 +300,7 @@ const Chat: React.FC<Props> = ({
|
||||
question: string,
|
||||
valid: boolean,
|
||||
) => {
|
||||
reportMsgEvent(question, valid);
|
||||
if (!isMobile) {
|
||||
conversationRef?.current?.updateData();
|
||||
}
|
||||
@@ -374,130 +322,87 @@ const Chat: React.FC<Props> = ({
|
||||
msg.msgData = data;
|
||||
const msgList = [...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])];
|
||||
setMessageList(msgList);
|
||||
updateChatFilter(data, msgList);
|
||||
}
|
||||
updateMessageContainerScroll(`${questionId}`);
|
||||
};
|
||||
|
||||
const onCheckMore = (data: MsgDataType) => {
|
||||
setMessageList([
|
||||
...messageList,
|
||||
{
|
||||
id: uuid(),
|
||||
msg: data.response.name,
|
||||
type: MessageTypeEnum.PLUGIN,
|
||||
msgData: data,
|
||||
},
|
||||
]);
|
||||
updateMessageContainerScroll();
|
||||
if (onCheckMoreDetail) {
|
||||
onCheckMoreDetail();
|
||||
}
|
||||
const onToggleHistoryVisible = () => {
|
||||
setHistoryVisible(!historyVisible);
|
||||
};
|
||||
|
||||
const onToggleCollapseBtn = () => {
|
||||
setConversationCollapsed(!conversationCollapsed);
|
||||
localStorage.setItem('CONVERSATION_COLLAPSED', `${!conversationCollapsed}`);
|
||||
};
|
||||
const onApplyAuth = (model: string) => {};
|
||||
|
||||
const getIdentityMsgText = (model?: ModelType) => {
|
||||
return model
|
||||
? `您好,我当前身份是【${model.name}】主题专家,我将尽力帮您解答相关问题~`
|
||||
: '您好,我将尽力帮您解答所有主题相关问题~';
|
||||
};
|
||||
|
||||
const onApplyAuth = (model: string) => {
|
||||
setApplyAuthModel(model);
|
||||
setApplyAuthVisible(true);
|
||||
};
|
||||
|
||||
const onAddConversation = () => {
|
||||
conversationRef.current?.onAddConversation();
|
||||
const onAddConversation = (agent?: AgentType) => {
|
||||
conversationRef.current?.onAddConversation({ agent });
|
||||
inputFocus();
|
||||
};
|
||||
|
||||
const onSelectAgent = (agent: AgentType) => {
|
||||
if (agent.id === currentAgent?.id) {
|
||||
return;
|
||||
}
|
||||
setCurrentAgent(agent);
|
||||
setMessageList([
|
||||
...messageList,
|
||||
{
|
||||
id: uuid(),
|
||||
type: MessageTypeEnum.TEXT,
|
||||
msg: `您好,智能助理【${agent.name}】将与您对话,可输入“/”切换助理`,
|
||||
},
|
||||
]);
|
||||
updateMessageContainerScroll();
|
||||
};
|
||||
|
||||
const sendMsg = (msg: string, modelId?: number) => {
|
||||
onSendMsg(msg, messageList, modelId);
|
||||
if (isMobile) {
|
||||
inputBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const onCloseConversation = () => {
|
||||
setHistoryVisible(false);
|
||||
};
|
||||
|
||||
const chatClass = classNames(styles.chat, {
|
||||
[styles.mobileMode]: isMobileMode,
|
||||
[styles.mobile]: isMobile,
|
||||
[styles.copilotFullscreen]: copilotFullscreen,
|
||||
[styles.conversationCollapsed]: conversationCollapsed,
|
||||
[styles.historyVisible]: historyVisible,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={chatClass}>
|
||||
{!isMobileMode && <Helmet title={WEB_TITLE} />}
|
||||
<div className={styles.topSection} />
|
||||
{isMobile && <Helmet title={MOBILE_TITLE} />}
|
||||
<div className={styles.chatSection}>
|
||||
<Conversation
|
||||
currentConversation={currentConversation}
|
||||
collapsed={conversationCollapsed}
|
||||
isCopilotMode={isCopilotMode}
|
||||
defaultModelName={defaultModelName}
|
||||
defaultEntityFilter={defaultEntityFilter}
|
||||
triggerNewConversation={triggerNewConversation}
|
||||
onNewConversationTriggered={onNewConversationTriggered}
|
||||
onSelectConversation={onSelectConversation}
|
||||
ref={conversationRef}
|
||||
/>
|
||||
{!isMobile && (
|
||||
<AgentList
|
||||
agentList={agentList}
|
||||
currentAgent={currentAgent}
|
||||
onSelectAgent={onSelectAgent}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.chatApp}>
|
||||
{currentConversation && (
|
||||
<div className={styles.chatBody}>
|
||||
<div className={styles.chatContent}>
|
||||
{currentAgent && !isMobile && (
|
||||
<div className={styles.chatHeader}>
|
||||
<div className={styles.chatHeaderTitle}>{currentAgent.name}</div>
|
||||
<div className={styles.chatHeaderTip}>{currentAgent.description}</div>
|
||||
</div>
|
||||
)}
|
||||
<MessageContainer
|
||||
id="messageContainer"
|
||||
messageList={messageList}
|
||||
chatId={currentConversation?.chatId}
|
||||
isMobileMode={isMobileMode}
|
||||
conversationCollapsed={conversationCollapsed}
|
||||
copilotFullscreen={copilotFullscreen}
|
||||
agentList={agentList}
|
||||
onClickMessageContainer={inputFocus}
|
||||
historyVisible={historyVisible}
|
||||
currentAgent={currentAgent}
|
||||
chatVisible={chatVisible}
|
||||
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}
|
||||
copilotFullscreen={copilotFullscreen}
|
||||
onToggleCollapseBtn={onToggleCollapseBtn}
|
||||
onToggleHistoryVisible={onToggleHistoryVisible}
|
||||
onInputMsgChange={onInputMsgChange}
|
||||
onSendMsg={(msg: string, modelId?: number) => {
|
||||
onSendMsg(msg, messageList, modelId);
|
||||
if (isMobile) {
|
||||
inputBlur();
|
||||
}
|
||||
}}
|
||||
onSendMsg={sendMsg}
|
||||
onAddConversation={onAddConversation}
|
||||
onCancelDefaultFilter={() => {
|
||||
changeModel(undefined);
|
||||
setDefaultEntity(undefined);
|
||||
if (onCancelCopilotFilter) {
|
||||
onCancelCopilotFilter();
|
||||
}
|
||||
}}
|
||||
onSelectAgent={onSelectAgent}
|
||||
ref={chatFooterRef}
|
||||
/>
|
||||
@@ -505,6 +410,19 @@ const Chat: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Conversation
|
||||
agentList={agentList}
|
||||
currentAgent={currentAgent}
|
||||
currentConversation={currentConversation}
|
||||
historyVisible={historyVisible}
|
||||
isCopilotMode={isCopilotMode}
|
||||
defaultEntityFilter={defaultEntityFilter}
|
||||
triggerNewConversation={triggerNewConversation}
|
||||
onNewConversationTriggered={onNewConversationTriggered}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onCloseConversation={onCloseConversation}
|
||||
ref={conversationRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { isMobile } from '@/utils/utils';
|
||||
import { request } from 'umi';
|
||||
import { AgentType, ModelType } from './type';
|
||||
|
||||
const prefix = '/api';
|
||||
const prefix = isMobile ? '/openapi' : '/api';
|
||||
|
||||
export function saveConversation(chatName: string) {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
export function saveConversation(chatName: string, agentId: number) {
|
||||
return request<Result<any>>(
|
||||
`${prefix}/chat/manage/save?chatName=${chatName}&agentId=${agentId}`,
|
||||
{
|
||||
method: 'POST',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function updateConversationName(chatName: string, chatId: number = 0) {
|
||||
@@ -20,8 +24,8 @@ export function deleteConversation(chatId: number) {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/delete?chatId=${chatId}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function getAllConversations() {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/getAll`);
|
||||
export function getAllConversations(agentId?: number) {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/getAll`, { params: { agentId } });
|
||||
}
|
||||
|
||||
export function getMiniProgramList(entityId: string, modelId: number) {
|
||||
|
||||
@@ -1,232 +1,60 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.chat {
|
||||
height: calc(100vh - 48px) !important;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, rgba(23, 74, 228, 0) 29.44%, rgba(23, 74, 228, 0.06) 100%),
|
||||
linear-gradient(90deg, #f3f3f7 0%, #f3f3f7 20%, #ebf0f9 60%, #f3f3f7 80%, #f3f3f7 100%);
|
||||
|
||||
.chatSection {
|
||||
display: flex;
|
||||
width: 100vw !important;
|
||||
height: calc(100vh - 48px) !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chatApp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100vw - 260px);
|
||||
height: calc(100vh - 48px) !important;
|
||||
padding-left: 6px;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
|
||||
.emptyHolder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
background: rgb(243 243 243);
|
||||
border-bottom: 1px solid rgb(228, 228, 228);
|
||||
|
||||
.conversationNameWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.conversationName {
|
||||
padding: 4px 12px;
|
||||
color: var(--text-color-third) !important;
|
||||
font-size: 14px !important;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.editIcon {
|
||||
margin-left: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
margin-left: 12px;
|
||||
background-color: var(--text-color-fourth);
|
||||
}
|
||||
}
|
||||
|
||||
.conversationInput {
|
||||
width: 300px;
|
||||
color: var(--text-color-third) !important;
|
||||
font-size: 14px !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chatBody {
|
||||
.chatApp {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
|
||||
.chatContent {
|
||||
.chatBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.messageContainer {
|
||||
.chatContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
|
||||
.messageList {
|
||||
.chatHeader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 20px 60px 4px;
|
||||
row-gap: 16px;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: rgba(243, 243, 247, 0.85);
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
.messageItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
.chatHeaderTitle {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
: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 {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.ss-chat-table-even-row {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 16px 0 0;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-prev,
|
||||
.ant-pagination .ant-pagination-next {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
.ant-pagination-item-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-jump-prev,
|
||||
.ant-pagination-jump-next {
|
||||
.ant-pagination-item-link {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-options {
|
||||
display: inline-block;
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item-active {
|
||||
font-weight: 600;
|
||||
background-color: #ffffff;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5715;
|
||||
list-style: none;
|
||||
font-feature-settings: 'tnum', 'tnum';
|
||||
}
|
||||
}
|
||||
.chatHeaderTip {
|
||||
width: 600px;
|
||||
margin-left: 16px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,541 +62,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.conversationCollapsed {
|
||||
.chatApp {
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobileMode {
|
||||
height: 100% !important;
|
||||
&.historyVisible {
|
||||
.chatSection {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
height: 100% !important;
|
||||
|
||||
.conversationList {
|
||||
height: calc(100% - 50px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chatApp {
|
||||
width: calc(100% - 260px) !important;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&.copilotFullscreen {
|
||||
.chatApp {
|
||||
width: calc(100% - 260px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.conversationCollapsed {
|
||||
.chatApp {
|
||||
width: 100% !important;
|
||||
width: calc(100% - 707px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
height: 100vh !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);
|
||||
}
|
||||
.chatSection {
|
||||
.chatApp {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
|
||||
.leftSection {
|
||||
.searchConversation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.copilotMode {
|
||||
&.collapsed {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobileMode {
|
||||
.messageList {
|
||||
padding: 20px 12px 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.messageTime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modules {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 8px 12px;
|
||||
overflow: hidden;
|
||||
background: rgb(243, 243, 243);
|
||||
border-top: 1px solid rgb(228, 228, 228);
|
||||
|
||||
.moduleType {
|
||||
width: 80px;
|
||||
margin-right: 12px;
|
||||
|
||||
:global {
|
||||
.ant-select-selection-item {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
.ant-select-selection-item,
|
||||
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector::after {
|
||||
line-height: 28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.moduleSelect {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
word-break: break-all;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
height: 30px !important;
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
margin-top: -4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.example {
|
||||
margin-right: 4px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:global {
|
||||
button[ant-click-animating-without-extra-node]:after {
|
||||
border: 0 none;
|
||||
opacity: 0;
|
||||
animation: none 0 ease 0 1 normal;
|
||||
}
|
||||
|
||||
.iconBtn {
|
||||
color: rgba(0, 0, 0, 0.4) !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.scrollerControlIcon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modulesInner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moduleItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
padding: 4px 11px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
background-clip: padding-box;
|
||||
border-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
&.activeModuleItem {
|
||||
color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&.cmdItem {
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.optGroupBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.recentSearchBar {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.optGroupTitle {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.recentSearch {
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.clearSearch {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.recentSearchOption {
|
||||
padding-left: 12px !important;
|
||||
|
||||
.optionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 483px;
|
||||
|
||||
.removeRecentMsg {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.removeRecentMsg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
color: var(--text-color-third);
|
||||
column-gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.loadingWords {
|
||||
padding: 40px 1px;
|
||||
}
|
||||
|
||||
.associateWordsOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
|
||||
.optionContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 450px;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.indicatorItem {
|
||||
min-width: 180px;
|
||||
.indicatorLabel {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.indicatorValue {
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.autoCompleteDropdown {
|
||||
width: 650px !important;
|
||||
min-width: 650px !important;
|
||||
border-radius: 10px;
|
||||
|
||||
:global {
|
||||
.ant-select-item {
|
||||
min-height: 36px !important;
|
||||
line-height: 26px !important;
|
||||
|
||||
&:not(:first-child):hover {
|
||||
background: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommendItemTitle {
|
||||
margin-right: 14px;
|
||||
padding: 4px 12px;
|
||||
background-color: var(--deep-background);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.refeshQuestions {
|
||||
cursor: pointer;
|
||||
|
||||
.reloadIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.recommendQuestions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 100%;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 18px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.currentTool {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24px 0 2px;
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
|
||||
.removeTool {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 6px;
|
||||
color: var(--text-color-fifth);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.associateOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.associateOptionAvatar {
|
||||
width: 32px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.optionContent {
|
||||
min-width: 330px;
|
||||
}
|
||||
|
||||
.optionIndicator {
|
||||
min-width: 120px;
|
||||
margin-left: 4px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.messageLoading {
|
||||
margin-top: 30px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
:global {
|
||||
@@ -782,9 +90,4 @@
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-chat-item-typing-bubble {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export type ConversationDetailType = {
|
||||
initMsg?: string;
|
||||
modelId?: number;
|
||||
entityId?: string;
|
||||
agent?: AgentType;
|
||||
};
|
||||
|
||||
export enum MessageModeEnum {
|
||||
|
||||
11
webapp/packages/supersonic-fe/src/pages/Copilot/constants.ts
Normal file
11
webapp/packages/supersonic-fe/src/pages/Copilot/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const MODEL_PATH_MAP = {
|
||||
song: '歌曲库',
|
||||
'song-detail': '歌曲库',
|
||||
singer: '艺人库',
|
||||
'singer-detail': '艺人库',
|
||||
album: '专辑库',
|
||||
'album-detail': '专辑库',
|
||||
'digital-album': '专辑库',
|
||||
brand: '厂牌库',
|
||||
'brand-detail': '厂牌库',
|
||||
};
|
||||
@@ -1,36 +1,30 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import {
|
||||
CaretRightOutlined,
|
||||
CloseOutlined,
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import { CaretRightOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Chat from '../Chat';
|
||||
import { ModelType } from '../Chat/type';
|
||||
import { DefaultEntityType, ModelType } from '../Chat/type';
|
||||
import styles from './style.less';
|
||||
import { useDispatch } from 'umi';
|
||||
|
||||
type Props = {
|
||||
globalCopilotFilter: DefaultEntityType;
|
||||
copilotSendMsg: string;
|
||||
};
|
||||
|
||||
const Copilot: React.FC<Props> = ({ copilotSendMsg }) => {
|
||||
const Copilot: React.FC<Props> = ({ globalCopilotFilter, copilotSendMsg }) => {
|
||||
const [chatVisible, setChatVisible] = useState(false);
|
||||
const [defaultModelName, setDefaultModelName] = useState('');
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [defaultEntityFilter, setDefaultEntityFilter] = useState<DefaultEntityType>();
|
||||
const [triggerNewConversation, setTriggerNewConversation] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const chatVisibleValue = localStorage.getItem('CHAT_VISIBLE') === 'true';
|
||||
if (chatVisibleValue) {
|
||||
setTimeout(() => {
|
||||
setChatVisible(true);
|
||||
}, 500);
|
||||
if (globalCopilotFilter && globalCopilotFilter.entityId !== defaultEntityFilter?.entityId) {
|
||||
setTriggerNewConversation(true);
|
||||
}
|
||||
}, []);
|
||||
setDefaultEntityFilter(globalCopilotFilter);
|
||||
if (globalCopilotFilter?.modelName) {
|
||||
setDefaultModelName(globalCopilotFilter.modelName);
|
||||
}
|
||||
}, [globalCopilotFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copilotSendMsg) {
|
||||
@@ -41,118 +35,62 @@ const Copilot: React.FC<Props> = ({ copilotSendMsg }) => {
|
||||
|
||||
const updateChatVisible = (visible: boolean) => {
|
||||
setChatVisible(visible);
|
||||
localStorage.setItem('CHAT_VISIBLE', visible ? 'true' : 'false');
|
||||
};
|
||||
|
||||
const onToggleChatVisible = () => {
|
||||
const chatVisibleValue = !chatVisible;
|
||||
updateChatVisible(chatVisibleValue);
|
||||
if (!chatVisibleValue) {
|
||||
document.body.style.overflow = 'auto';
|
||||
} else {
|
||||
if (fullscreen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onCloseChat = () => {
|
||||
updateChatVisible(false);
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
|
||||
const onTransferChat = () => {
|
||||
window.open(
|
||||
`${window.location.href.includes('webapp') ? '/webapp' : ''}/chat?cid=${localStorage.getItem(
|
||||
'CONVERSATION_ID',
|
||||
)}${defaultModelName ? `&modelName=${defaultModelName}` : ''}`,
|
||||
);
|
||||
window.open('/chat');
|
||||
};
|
||||
|
||||
const onCurrentModelChange = (model?: ModelType) => {
|
||||
setDefaultModelName(model?.name || '');
|
||||
if (model?.name !== defaultModelName) {
|
||||
onCancelCopilotFilter();
|
||||
}
|
||||
};
|
||||
|
||||
const onEnterFullscreen = () => {
|
||||
setFullscreen(true);
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
const onExitFullscreen = () => {
|
||||
setFullscreen(false);
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
|
||||
const onCheckMoreDetail = () => {
|
||||
if (!fullscreen) {
|
||||
onEnterFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
const onCancelCopilotFilter = () => {
|
||||
dispatch({
|
||||
type: 'globalState/setGlobalCopilotFilter',
|
||||
payload: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const onNewConversationTriggered = () => {
|
||||
setTriggerNewConversation(false);
|
||||
};
|
||||
|
||||
const chatPopoverClass = classNames(styles.chatPopover, {
|
||||
[styles.fullscreen]: fullscreen,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.copilot} onClick={onToggleChatVisible}>
|
||||
<IconFont type="icon-copilot-fill" />
|
||||
</div>
|
||||
{chatVisible && (
|
||||
<div className={styles.copilotContent}>
|
||||
<div className={chatPopoverClass}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.leftSection}>
|
||||
<CloseOutlined className={styles.close} onClick={onCloseChat} />
|
||||
{fullscreen ? (
|
||||
<FullscreenExitOutlined
|
||||
className={styles.fullscreen}
|
||||
onClick={onExitFullscreen}
|
||||
/>
|
||||
) : (
|
||||
<FullscreenOutlined className={styles.fullscreen} onClick={onEnterFullscreen} />
|
||||
)}
|
||||
<IconFont
|
||||
type="icon-weibiaoti-"
|
||||
className={styles.transfer}
|
||||
onClick={onTransferChat}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>Copilot</div>
|
||||
</div>
|
||||
<div className={styles.chat}>
|
||||
<Chat
|
||||
defaultModelName={defaultModelName}
|
||||
copilotSendMsg={copilotSendMsg}
|
||||
isCopilotMode
|
||||
copilotFullscreen={fullscreen}
|
||||
triggerNewConversation={triggerNewConversation}
|
||||
onNewConversationTriggered={onNewConversationTriggered}
|
||||
onCurrentModelChange={onCurrentModelChange}
|
||||
onCancelCopilotFilter={onCancelCopilotFilter}
|
||||
onCheckMoreDetail={onCheckMoreDetail}
|
||||
<div className={styles.copilotContent} style={{ display: chatVisible ? 'block' : 'none' }}>
|
||||
<div className={styles.chatPopover}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.leftSection}>
|
||||
<CloseOutlined className={styles.close} onClick={onCloseChat} />
|
||||
<IconFont
|
||||
type="icon-weibiaoti-"
|
||||
className={styles.transfer}
|
||||
onClick={onTransferChat}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>内容库问答</div>
|
||||
</div>
|
||||
<div className={styles.chat}>
|
||||
<Chat
|
||||
copilotSendMsg={copilotSendMsg}
|
||||
defaultModelName={defaultModelName}
|
||||
defaultEntityFilter={defaultEntityFilter}
|
||||
triggerNewConversation={triggerNewConversation}
|
||||
chatVisible={chatVisible}
|
||||
isCopilotMode
|
||||
onNewConversationTriggered={onNewConversationTriggered}
|
||||
onCurrentModelChange={onCurrentModelChange}
|
||||
/>
|
||||
</div>
|
||||
<CaretRightOutlined className={styles.rightArrow} />
|
||||
</div>
|
||||
)}
|
||||
<CaretRightOutlined className={styles.rightArrow} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,10 +34,11 @@
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50vw;
|
||||
width: 70vw;
|
||||
min-width: 1100px;
|
||||
height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 8px 8px 20px rgba(55, 99, 170, 0.1), -2px -2px 16px rgba(55, 99, 170, 0.1);
|
||||
box-shadow: 4px 4px 10px rgba(55, 99, 170, 0.3), -2px -2px 16px rgba(55, 99, 170, 0.3);
|
||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out,
|
||||
-webkit-transform 0.3s ease-in-out;
|
||||
|
||||
@@ -47,16 +48,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
height: 40px;
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
background: linear-gradient(90deg, #4692ff 0%, #1877ff 98%);
|
||||
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
|
||||
box-shadow: 1px 1px 8px #1b4aef5c;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
@@ -85,13 +86,13 @@
|
||||
}
|
||||
|
||||
.chat {
|
||||
height: calc(90vh - 50px);
|
||||
height: calc(90vh - 40px);
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: calc(100vw - 90px);
|
||||
left: 60px;
|
||||
width: calc(100vw - 150px);
|
||||
height: 100vh;
|
||||
|
||||
.chat {
|
||||
|
||||
13
webapp/start-fe-dev.bat
Normal file
13
webapp/start-fe-dev.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
rmdir /s /q ".\packages\supersonic-fe\src\.umi"
|
||||
rmdir /s /q ".\packages\supersonic-fe\src\.umi-production"
|
||||
|
||||
call npm i
|
||||
|
||||
call npx lerna add supersonic-chat-sdk --scope supersonic-fe
|
||||
|
||||
call npx lerna bootstrap
|
||||
|
||||
call npx lerna exec --scope supersonic-chat-sdk npm run build
|
||||
|
||||
call npx lerna exec --scope supersonic-fe npm start
|
||||
22
webapp/start-fe-prod.bat
Normal file
22
webapp/start-fe-prod.bat
Normal file
@@ -0,0 +1,22 @@
|
||||
setlocal
|
||||
|
||||
|
||||
call npm i
|
||||
|
||||
call npx lerna add supersonic-chat-sdk --scope supersonic-fe
|
||||
|
||||
call npx lerna bootstrap
|
||||
|
||||
call npx lerna exec --scope supersonic-chat-sdk npm run build
|
||||
|
||||
call npx lerna exec --scope supersonic-fe npm run build:os-local
|
||||
|
||||
cd packages\supersonic-fe
|
||||
|
||||
tar -zcvf supersonic-webapp.tar.gz supersonic-webapp
|
||||
|
||||
move supersonic-webapp.tar.gz ..\..\
|
||||
|
||||
cd ..
|
||||
|
||||
endlocal
|
||||
Reference in New Issue
Block a user