Merge pull request #31 from williamhliu/master

[feature](webapp) add copilot and modify domain to model
This commit is contained in:
williamhliu
2023-08-15 20:11:12 +08:00
committed by GitHub
43 changed files with 738 additions and 431 deletions

View File

@@ -22,7 +22,7 @@ module.exports = function (proxy, allowedHost) {
// https://github.com/webpack/webpack-dev-server/issues/887 // https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud // However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated: // environment or submodels in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271 // https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233 // https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a // While we're investigating better solutions, for now we will take a
@@ -33,7 +33,7 @@ module.exports = function (proxy, allowedHost) {
// So we will disable the host check normally, but enable it if you have // So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you // specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable. // really know what you're doing with a special environment variable.
// Note: ["localhost", ".localhost"] will support subdomains - but we might // Note: ["localhost", ".localhost"] will support submodels - but we might
// want to allow setting the allowedHosts manually for more complex setups // want to allow setting the allowedHosts manually for more complex setups
allowedHosts: disableFirewall ? 'all' : [allowedHost], allowedHosts: disableFirewall ? 'all' : [allowedHost],
headers: { headers: {

View File

@@ -1,7 +1,7 @@
export type SearchRecommendItem = { export type SearchRecommendItem = {
complete: boolean; complete: boolean;
domainId: number; modelId: number;
domainName: string; modelName: string;
recommend: string; recommend: string;
subRecommend: string; subRecommend: string;
schemaElementType: string; schemaElementType: string;
@@ -12,12 +12,12 @@ export type FieldType = {
id: number; id: number;
name: string; name: string;
status: number; status: number;
domain: number; model: number;
type: string; type: string;
value: string; value: string;
}; };
export type DomainInfoType = { export type ModelInfoType = {
bizName: string; bizName: string;
itemId: number; itemId: number;
name: string; name: string;
@@ -27,7 +27,7 @@ export type DomainInfoType = {
}; };
export type EntityInfoType = { export type EntityInfoType = {
domainInfo: DomainInfoType; modelInfo: ModelInfoType;
dimensions: FieldType[]; dimensions: FieldType[];
metrics: FieldType[]; metrics: FieldType[];
entityId: number; entityId: number;
@@ -53,8 +53,8 @@ export type FilterItemType = {
export type ChatContextType = { export type ChatContextType = {
aggType: string; aggType: string;
domainId: number; modelId: number;
domainName: string; modelName: string;
dateInfo: DateInfoType; dateInfo: DateInfoType;
dimensions: FieldType[]; dimensions: FieldType[];
metrics: FieldType[]; metrics: FieldType[];
@@ -104,6 +104,7 @@ export type MsgDataType = {
queryMode: string; queryMode: string;
queryState: string; queryState: string;
response: PluginResonseType; response: PluginResonseType;
parseOptions?: ChatContextType[];
}; };
export enum ParseStateEnum { export enum ParseStateEnum {
@@ -121,6 +122,7 @@ export type ParseDataType = {
} }
export type QueryDataType = { export type QueryDataType = {
aggregateInfo: AggregateInfoType;
queryColumns: ColumnType[]; queryColumns: ColumnType[];
queryResults: any[]; queryResults: any[];
}; };
@@ -153,7 +155,7 @@ export const SEMANTIC_TYPE_MAP = {
}; };
export type SuggestionItemType = { export type SuggestionItemType = {
domain: number; model: number;
name: string; name: string;
bizName: string bizName: string
}; };
@@ -187,7 +189,7 @@ export type HistoryType = {
export type DrillDownDimensionType = { export type DrillDownDimensionType = {
id: number; id: number;
domain: number; model: number;
name: string; name: string;
bizName: string; bizName: string;
} }

View File

@@ -10,6 +10,7 @@ type Props = {
parseInfoOptions: ChatContextType[]; parseInfoOptions: ChatContextType[];
parseTip: string; parseTip: string;
currentParseInfo?: ChatContextType; currentParseInfo?: ChatContextType;
optionMode?: boolean;
onSelectParseInfo: (parseInfo: ChatContextType) => void; onSelectParseInfo: (parseInfo: ChatContextType) => void;
}; };
@@ -20,6 +21,7 @@ const ParseTip: React.FC<Props> = ({
parseInfoOptions, parseInfoOptions,
parseTip, parseTip,
currentParseInfo, currentParseInfo,
optionMode,
onSelectParseInfo, onSelectParseInfo,
}) => { }) => {
const prefixCls = `${PREFIX_CLS}-item`; const prefixCls = `${PREFIX_CLS}-item`;
@@ -38,7 +40,7 @@ const ParseTip: React.FC<Props> = ({
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => { const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
const { const {
domainName, modelName,
dateInfo, dateInfo,
dimensionFilters, dimensionFilters,
dimensions, dimensions,
@@ -70,6 +72,7 @@ const ParseTip: React.FC<Props> = ({
[`${prefixCls}-tip-item-option`]: isOptions, [`${prefixCls}-tip-item-option`]: isOptions,
}); });
const entityId = dimensionFilters?.length > 0 ? dimensionFilters[0].value : undefined;
const entityAlias = entity?.alias?.[0]?.split('.')?.[0]; const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name; const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
@@ -106,7 +109,10 @@ const ParseTip: React.FC<Props> = ({
</div> </div>
) : ( ) : (
<> <>
{queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL' ? ( {queryMode.includes('ENTITY') &&
typeof entityId === 'string' &&
!!entityAlias &&
!!entityName ? (
<div className={`${prefixCls}-tip-item`}> <div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}</div> <div className={`${prefixCls}-tip-item-name`}>{entityAlias}</div>
<div className={itemValueClass}>{entityName}</div> <div className={itemValueClass}>{entityName}</div>
@@ -114,7 +120,7 @@ const ParseTip: React.FC<Props> = ({
) : ( ) : (
<div className={`${prefixCls}-tip-item`}> <div className={`${prefixCls}-tip-item`}>
<div className={`${prefixCls}-tip-item-name`}></div> <div className={`${prefixCls}-tip-item-name`}></div>
<div className={itemValueClass}>{domainName}</div> <div className={itemValueClass}>{modelName}</div>
</div> </div>
)} )}
{modeName === '算指标' && metric && ( {modeName === '算指标' && metric && (
@@ -180,10 +186,12 @@ const ParseTip: React.FC<Props> = ({
let tipNode: ReactNode; let tipNode: ReactNode;
if (parseInfoOptions.length > 1) { if (parseInfoOptions.length > 1 || optionMode) {
tipNode = ( tipNode = (
<div className={`${prefixCls}-multi-options`}> <div className={`${prefixCls}-multi-options`}>
<div></div> <div>
<strong></strong>
</div>
<div className={`${prefixCls}-options`}> <div className={`${prefixCls}-options`}>
{parseInfoOptions.map((item, index) => getTipNode(item, true, index))} {parseInfoOptions.map((item, index) => getTipNode(item, true, index))}
</div> </div>

View File

@@ -9,12 +9,13 @@ import ExecuteItem from './ExecuteItem';
type Props = { type Props = {
msg: string; msg: string;
conversationId?: number; conversationId?: number;
domainId?: number; modelId?: number;
filter?: any[]; filter?: any[];
isLastMessage?: boolean; isLastMessage?: boolean;
msgData?: MsgDataType; msgData?: MsgDataType;
isMobileMode?: boolean; isMobileMode?: boolean;
triggerResize?: boolean; triggerResize?: boolean;
parseOptions?: ChatContextType[];
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void; onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
onUpdateMessageScroll?: () => void; onUpdateMessageScroll?: () => void;
}; };
@@ -22,19 +23,20 @@ type Props = {
const ChatItem: React.FC<Props> = ({ const ChatItem: React.FC<Props> = ({
msg, msg,
conversationId, conversationId,
domainId, modelId,
filter, filter,
isLastMessage, isLastMessage,
isMobileMode, isMobileMode,
triggerResize, triggerResize,
msgData, msgData,
parseOptions,
onMsgDataLoaded, onMsgDataLoaded,
onUpdateMessageScroll, onUpdateMessageScroll,
}) => { }) => {
const [data, setData] = useState<MsgDataType>(); const [data, setData] = useState<MsgDataType>();
const [parseLoading, setParseLoading] = useState(false); const [parseLoading, setParseLoading] = useState(false);
const [parseInfo, setParseInfo] = useState<ChatContextType>(); const [parseInfo, setParseInfo] = useState<ChatContextType>();
const [parseInfoOptions, setParseInfoOptions] = useState<ChatContextType[]>([]); const [parseInfoOptions, setParseInfoOptions] = useState<ChatContextType[]>(parseOptions || []);
const [parseTip, setParseTip] = useState(''); const [parseTip, setParseTip] = useState('');
const [executeLoading, setExecuteLoading] = useState(false); const [executeLoading, setExecuteLoading] = useState(false);
const [executeTip, setExecuteTip] = useState(''); const [executeTip, setExecuteTip] = useState('');
@@ -68,20 +70,43 @@ const ChatItem: React.FC<Props> = ({
return true; return true;
}; };
const onExecute = async (parseInfoValue: ChatContextType, isSwitch?: boolean) => { const onExecute = async (
parseInfoValue: ChatContextType,
parseInfoOptions?: ChatContextType[]
) => {
setExecuteMode(true); setExecuteMode(true);
setExecuteLoading(true); setExecuteLoading(true);
const { data } = await chatExecute(msg, conversationId!, parseInfoValue); const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
setExecuteLoading(false); setExecuteLoading(false);
const valid = updateData(data); const valid = updateData(data);
if (onMsgDataLoaded && !isSwitch) { if (onMsgDataLoaded) {
onMsgDataLoaded({ ...data.data, chatContext: parseInfoValue }, valid); 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
);
} }
}; };
const onSendMsg = async () => { const onSendMsg = async () => {
setParseLoading(true); setParseLoading(true);
const { data: parseData } = await chatParse(msg, conversationId, domainId, filter); const { data: parseData } = await chatParse(msg, conversationId, modelId, filter);
setParseLoading(false); setParseLoading(false);
const { code, data } = parseData || {}; const { code, data } = parseData || {};
const { state, selectedParses } = data || {}; const { state, selectedParses } = data || {};
@@ -91,7 +116,7 @@ const ChatItem: React.FC<Props> = ({
selectedParses == null || selectedParses == null ||
selectedParses.length === 0 || selectedParses.length === 0 ||
(selectedParses.length === 1 && (selectedParses.length === 1 &&
!selectedParses[0]?.domainName && !selectedParses[0]?.modelName &&
!selectedParses[0]?.properties?.CONTEXT?.plugin?.name && !selectedParses[0]?.properties?.CONTEXT?.plugin?.name &&
selectedParses[0]?.queryMode !== 'WEB_PAGE') selectedParses[0]?.queryMode !== 'WEB_PAGE')
) { ) {
@@ -102,15 +127,13 @@ const ChatItem: React.FC<Props> = ({
onUpdateMessageScroll(); onUpdateMessageScroll();
} }
setParseInfoOptions(selectedParses || []); setParseInfoOptions(selectedParses || []);
if (selectedParses.length === 1) { const parseInfoValue = selectedParses[0];
const parseInfoValue = selectedParses[0]; setParseInfo(parseInfoValue);
setParseInfo(parseInfoValue); onExecute(parseInfoValue, selectedParses);
onExecute(parseInfoValue);
}
}; };
useEffect(() => { useEffect(() => {
if (data !== undefined) { if (data !== undefined || parseOptions !== undefined || executeTip !== '') {
return; return;
} }
if (msgData) { if (msgData) {
@@ -124,7 +147,7 @@ const ChatItem: React.FC<Props> = ({
const onSwitchEntity = async (entityId: string) => { const onSwitchEntity = async (entityId: string) => {
setEntitySwitching(true); setEntitySwitching(true);
const res = await switchEntity(entityId, data?.chatContext?.domainId, conversationId || 0); const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
setEntitySwitching(false); setEntitySwitching(false);
setData(res.data.data); setData(res.data.data);
}; };
@@ -135,7 +158,7 @@ const ChatItem: React.FC<Props> = ({
const onSelectParseInfo = async (parseInfoValue: ChatContextType) => { const onSelectParseInfo = async (parseInfoValue: ChatContextType) => {
setParseInfo(parseInfoValue); setParseInfo(parseInfoValue);
onExecute(parseInfoValue, parseInfo !== undefined); onExecute(parseInfoValue);
if (onUpdateMessageScroll) { if (onUpdateMessageScroll) {
onUpdateMessageScroll(); onUpdateMessageScroll();
} }
@@ -148,9 +171,10 @@ const ChatItem: React.FC<Props> = ({
<div className={`${prefixCls}-content`}> <div className={`${prefixCls}-content`}>
<ParseTip <ParseTip
parseLoading={parseLoading} parseLoading={parseLoading}
parseInfoOptions={parseInfoOptions} parseInfoOptions={parseOptions || parseInfoOptions.slice(0, 1)}
parseTip={parseTip} parseTip={parseTip}
currentParseInfo={parseInfo} currentParseInfo={parseInfo}
optionMode={parseOptions !== undefined}
onSelectParseInfo={onSelectParseInfo} onSelectParseInfo={onSelectParseInfo}
/> />
</div> </div>

View File

@@ -1,11 +1,11 @@
import { PREFIX_CLS } from '../../../common/constants'; import { PREFIX_CLS } from '../../../common/constants';
type Props = { type Props = {
domain: string; model: string;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const ApplyAuth: React.FC<Props> = ({ domain, onApplyAuth }) => { const ApplyAuth: React.FC<Props> = ({ model, onApplyAuth }) => {
const prefixCls = `${PREFIX_CLS}-apply-auth`; const prefixCls = `${PREFIX_CLS}-apply-auth`;
return ( return (
@@ -15,7 +15,7 @@ const ApplyAuth: React.FC<Props> = ({ domain, onApplyAuth }) => {
<span <span
className={`${prefixCls}-apply`} className={`${prefixCls}-apply`}
onClick={() => { onClick={() => {
onApplyAuth(domain); onApplyAuth(model);
}} }}
> >

View File

@@ -15,7 +15,7 @@ type Props = {
drillDownDimension?: DrillDownDimensionType; drillDownDimension?: DrillDownDimensionType;
loading: boolean; loading: boolean;
onSelectDimension: (dimension?: DrillDownDimensionType) => void; onSelectDimension: (dimension?: DrillDownDimensionType) => void;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const BarChart: React.FC<Props> = ({ const BarChart: React.FC<Props> = ({
@@ -152,7 +152,7 @@ const BarChart: React.FC<Props> = ({
if (metricColumn && !metricColumn?.authorized) { if (metricColumn && !metricColumn?.authorized) {
return ( return (
<NoPermissionChart <NoPermissionChart
domain={entityInfo?.domainInfo.name || ''} model={entityInfo?.modelInfo.name || ''}
chartType="barChart" chartType="barChart"
onApplyAuth={onApplyAuth} onApplyAuth={onApplyAuth}
/> />
@@ -193,11 +193,9 @@ const BarChart: React.FC<Props> = ({
<Spin spinning={loading}> <Spin spinning={loading}>
<div className={`${prefixCls}-chart`} ref={chartRef} /> <div className={`${prefixCls}-chart`} ref={chartRef} />
</Spin> </Spin>
{(queryMode === 'METRIC_DOMAIN' || {queryMode.includes('METRIC') && (
queryMode === 'METRIC_FILTER' ||
queryMode === 'METRIC_GROUPBY') && (
<DrillDownDimensions <DrillDownDimensions
domainId={chatContext.domainId} modelId={chatContext.modelId}
drillDownDimension={drillDownDimension} drillDownDimension={drillDownDimension}
dimensionFilters={chatContext.dimensionFilters} dimensionFilters={chatContext.dimensionFilters}
onSelectDimension={onSelectDimension} onSelectDimension={onSelectDimension}

View File

@@ -26,7 +26,7 @@ const Message: React.FC<Props> = ({
}) => { }) => {
const prefixCls = `${PREFIX_CLS}-message`; const prefixCls = `${PREFIX_CLS}-message`;
const { domainName, dateInfo, dimensionFilters } = chatContext || {}; const { modelName, dateInfo, dimensionFilters } = chatContext || {};
const { startDate, endDate } = dateInfo || {}; const { startDate, endDate } = dateInfo || {};
const entityInfoList = const entityInfoList =
@@ -67,7 +67,7 @@ const Message: React.FC<Props> = ({
<div className={`${prefixCls}-main-entity-info`}> <div className={`${prefixCls}-main-entity-info`}>
<div className={`${prefixCls}-info-item`}> <div className={`${prefixCls}-info-item`}>
<div className={`${prefixCls}-info-name`}></div> <div className={`${prefixCls}-info-name`}></div>
<div className={`${prefixCls}-info-value`}>{domainName}</div> <div className={`${prefixCls}-info-value`}>{modelName}</div>
</div> </div>
<div className={`${prefixCls}-info-item`}> <div className={`${prefixCls}-info-item`}>
<div className={`${prefixCls}-info-name`}></div> <div className={`${prefixCls}-info-name`}></div>

View File

@@ -10,7 +10,7 @@
margin-bottom: 6px; margin-bottom: 6px;
} }
&-domain-name { &-model-name {
color: var(--text-color); color: var(--text-color);
margin-left: 4px; margin-left: 4px;
font-weight: 500; font-weight: 500;

View File

@@ -13,7 +13,7 @@ type Props = {
drillDownDimension?: DrillDownDimensionType; drillDownDimension?: DrillDownDimensionType;
loading: boolean; loading: boolean;
onSelectDimension: (dimension?: DrillDownDimensionType) => void; onSelectDimension: (dimension?: DrillDownDimensionType) => void;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const MetricCard: React.FC<Props> = ({ const MetricCard: React.FC<Props> = ({
@@ -64,7 +64,7 @@ const MetricCard: React.FC<Props> = ({
<div className={indicatorClass}> <div className={indicatorClass}>
<div className={`${prefixCls}-date-range`}>{startDate}</div> <div className={`${prefixCls}-date-range`}>{startDate}</div>
{indicatorColumn && !indicatorColumn?.authorized ? ( {indicatorColumn && !indicatorColumn?.authorized ? (
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} /> <ApplyAuth model={entityInfo?.modelInfo.name || ''} onApplyAuth={onApplyAuth} />
) : ( ) : (
<div className={`${prefixCls}-indicator-value`}> <div className={`${prefixCls}-indicator-value`}>
{formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'} {formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'}
@@ -79,10 +79,10 @@ const MetricCard: React.FC<Props> = ({
)} )}
</div> </div>
</Spin> </Spin>
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && ( {queryMode.includes('METRIC') && (
<div className={`${prefixCls}-drill-down-dimensions`}> <div className={`${prefixCls}-drill-down-dimensions`}>
<DrillDownDimensions <DrillDownDimensions
domainId={chatContext.domainId} modelId={chatContext.modelId}
dimensionFilters={chatContext.dimensionFilters} dimensionFilters={chatContext.dimensionFilters}
drillDownDimension={drillDownDimension} drillDownDimension={drillDownDimension}
onSelectDimension={onSelectDimension} onSelectDimension={onSelectDimension}

View File

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

View File

@@ -14,17 +14,17 @@ import { ColumnType } from '../../../common/type';
import NoPermissionChart from '../NoPermissionChart'; import NoPermissionChart from '../NoPermissionChart';
type Props = { type Props = {
domain?: string; model?: string;
dateColumnName: string; dateColumnName: string;
categoryColumnName: string; categoryColumnName: string;
metricField: ColumnType; metricField: ColumnType;
resultList: any[]; resultList: any[];
triggerResize?: boolean; triggerResize?: boolean;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const MetricTrendChart: React.FC<Props> = ({ const MetricTrendChart: React.FC<Props> = ({
domain, model,
dateColumnName, dateColumnName,
categoryColumnName, categoryColumnName,
metricField, metricField,
@@ -204,7 +204,7 @@ const MetricTrendChart: React.FC<Props> = ({
return ( return (
<div> <div>
{!metricField.authorized ? ( {!metricField.authorized ? (
<NoPermissionChart domain={domain || ''} onApplyAuth={onApplyAuth} /> <NoPermissionChart model={model || ''} onApplyAuth={onApplyAuth} />
) : ( ) : (
<div className={`${prefixCls}-flow-trend-chart`} ref={chartRef} /> <div className={`${prefixCls}-flow-trend-chart`} ref={chartRef} />
)} )}

View File

@@ -15,7 +15,7 @@ type Props = {
data: MsgDataType; data: MsgDataType;
chartIndex: number; chartIndex: number;
triggerResize?: boolean; triggerResize?: boolean;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApplyAuth }) => { const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApplyAuth }) => {
@@ -36,6 +36,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption); const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
const [dimensions, setDimensions] = useState<FieldType[]>(chatContext?.dimensions); const [dimensions, setDimensions] = useState<FieldType[]>(chatContext?.dimensions);
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>(); const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
const [aggregateInfoValue, setAggregateInfoValue] = useState<any>(aggregateInfo);
const [dateModeValue, setDateModeValue] = useState(dateMode); const [dateModeValue, setDateModeValue] = useState(dateMode);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -72,6 +73,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
if (data.code === 200) { if (data.code === 200) {
setColumns(data.data?.queryColumns || []); setColumns(data.data?.queryColumns || []);
setDataSource(data.data?.queryResults || []); setDataSource(data.data?.queryResults || []);
setAggregateInfoValue(data.data?.aggregateInfo);
} }
}; };
@@ -172,7 +174,9 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
</div> </div>
)} )}
</div> </div>
{aggregateInfo?.metricInfos?.length > 0 && <MetricInfo aggregateInfo={aggregateInfo} />} {aggregateInfoValue?.metricInfos?.length > 0 && (
<MetricInfo aggregateInfo={aggregateInfoValue} />
)}
<div className={`${prefixCls}-date-options`}> <div className={`${prefixCls}-date-options`}>
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => { {dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
const dateOptionClass = classNames(`${prefixCls}-date-option`, { const dateOptionClass = classNames(`${prefixCls}-date-option`, {
@@ -205,7 +209,7 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} /> <Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
) : ( ) : (
<MetricTrendChart <MetricTrendChart
domain={entityInfo?.domainInfo.name} model={entityInfo?.modelInfo.name}
dateColumnName={dateColumnName} dateColumnName={dateColumnName}
categoryColumnName={categoryColumnName} categoryColumnName={categoryColumnName}
metricField={currentMetricField} metricField={currentMetricField}
@@ -215,11 +219,9 @@ const MetricTrend: React.FC<Props> = ({ data, chartIndex, triggerResize, onApply
/> />
)} )}
</Spin> </Spin>
{(queryMode === 'METRIC_DOMAIN' || {queryMode.includes('METRIC') && (
queryMode === 'METRIC_FILTER' ||
queryMode === 'METRIC_GROUPBY') && (
<DrillDownDimensions <DrillDownDimensions
domainId={chatContext.domainId} modelId={chatContext.modelId}
drillDownDimension={drillDownDimension} drillDownDimension={drillDownDimension}
dimensionFilters={chatContext.dimensionFilters} dimensionFilters={chatContext.dimensionFilters}
onSelectDimension={onSelectDimension} onSelectDimension={onSelectDimension}

View File

@@ -3,12 +3,12 @@ import { CLS_PREFIX } from '../../../common/constants';
import ApplyAuth from '../ApplyAuth'; import ApplyAuth from '../ApplyAuth';
type Props = { type Props = {
domain: string; model: string;
chartType?: string; chartType?: string;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const NoPermissionChart: React.FC<Props> = ({ domain, chartType, onApplyAuth }) => { const NoPermissionChart: React.FC<Props> = ({ model, chartType, onApplyAuth }) => {
const prefixCls = `${CLS_PREFIX}-no-permission-chart`; const prefixCls = `${CLS_PREFIX}-no-permission-chart`;
const chartHolderClass = classNames(`${prefixCls}-holder`, { const chartHolderClass = classNames(`${prefixCls}-holder`, {
@@ -19,7 +19,7 @@ const NoPermissionChart: React.FC<Props> = ({ domain, chartType, onApplyAuth })
<div className={prefixCls}> <div className={prefixCls}>
<div className={chartHolderClass} /> <div className={chartHolderClass} />
<div className={`${prefixCls}-no-permission`}> <div className={`${prefixCls}-no-permission`}>
<ApplyAuth domain={domain} onApplyAuth={onApplyAuth} /> <ApplyAuth model={model} onApplyAuth={onApplyAuth} />
</div> </div>
</div> </div>
); );

View File

@@ -8,7 +8,7 @@ import { SizeType } from 'antd/es/config-provider/SizeContext';
type Props = { type Props = {
data: MsgDataType; data: MsgDataType;
size?: SizeType; size?: SizeType;
onApplyAuth?: (domain: string) => void; onApplyAuth?: (model: string) => void;
}; };
const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => { const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => {
@@ -24,9 +24,7 @@ const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => {
title: name || nameEn, title: name || nameEn,
render: (value: string | number) => { render: (value: string | number) => {
if (!authorized) { if (!authorized) {
return ( return <ApplyAuth model={entityInfo?.modelInfo.name || ''} onApplyAuth={onApplyAuth} />;
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
);
} }
if (dataFormatType === 'percent') { if (dataFormatType === 'percent') {
return ( return (
@@ -71,7 +69,7 @@ const Table: React.FC<Props> = ({ data, size, onApplyAuth }) => {
columns={tableColumns} columns={tableColumns}
dataSource={queryResults} dataSource={queryResults}
style={{ width: '100%' }} style={{ width: '100%' }}
scroll={{ x: 'max-content' }} // scroll={{ x: 'max-content' }}
rowClassName={getRowClassName} rowClassName={getRowClassName}
size={size} size={size}
/> />

View File

@@ -3,7 +3,7 @@
@table-prefix-cls: ~'@{supersonic-chat-prefix}-table'; @table-prefix-cls: ~'@{supersonic-chat-prefix}-table';
.@{table-prefix-cls} { .@{table-prefix-cls} {
margin-top: 20px; margin-top: 16px;
margin-bottom: 20px; margin-bottom: 20px;
&-photo { &-photo {

View File

@@ -35,10 +35,15 @@ const ChatMsg: React.FC<Props> = ({ question, data, chartIndex, isMobileMode, tr
const metricFields = columns.filter(item => item.showType === 'NUMBER'); const metricFields = columns.filter(item => item.showType === 'NUMBER');
const isMetricCard = const isMetricCard =
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && queryMode.includes('METRIC') &&
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate); (singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
const isText = columns.length === 1 && columns[0].showType === 'CATEGORY' && singleData; const isText =
columns.length === 1 &&
columns[0].showType === 'CATEGORY' &&
!queryMode.includes('METRIC') &&
!queryMode.includes('ENTITY') &&
singleData;
const onLoadData = async (value: any) => { const onLoadData = async (value: any) => {
setLoading(true); setLoading(true);

View File

@@ -7,7 +7,7 @@ import { DownOutlined } from '@ant-design/icons';
import classNames from 'classnames'; import classNames from 'classnames';
type Props = { type Props = {
domainId: number; modelId: number;
drillDownDimension?: DrillDownDimensionType; drillDownDimension?: DrillDownDimensionType;
isMetricCard?: boolean; isMetricCard?: boolean;
dimensionFilters?: FilterItemType[]; dimensionFilters?: FilterItemType[];
@@ -17,7 +17,7 @@ type Props = {
const MAX_DIMENSION_COUNT = 20; const MAX_DIMENSION_COUNT = 20;
const DrillDownDimensions: React.FC<Props> = ({ const DrillDownDimensions: React.FC<Props> = ({
domainId, modelId,
drillDownDimension, drillDownDimension,
isMetricCard, isMetricCard,
dimensionFilters, dimensionFilters,
@@ -30,7 +30,7 @@ const DrillDownDimensions: React.FC<Props> = ({
const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`; const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`;
const initData = async () => { const initData = async () => {
const res = await queryDrillDownDimensions(domainId); const res = await queryDrillDownDimensions(modelId);
setDimensions( setDimensions(
res.data.data.dimensions res.data.data.dimensions
.filter(dimension => !dimensionFilters?.some(filter => filter.name === dimension.name)) .filter(dimension => !dimensionFilters?.some(filter => filter.name === dimension.name))

View File

@@ -10,16 +10,16 @@ import classNames from 'classnames';
type Props = { type Props = {
entityId: string | number; entityId: string | number;
domainId: number; modelId: number;
domainName: string; modelName: string;
isMobileMode?: boolean; isMobileMode?: boolean;
onSelect: (option: string) => void; onSelect: (option: string) => void;
}; };
const RecommendOptions: React.FC<Props> = ({ const RecommendOptions: React.FC<Props> = ({
entityId, entityId,
domainId, modelId,
domainName, modelName,
isMobileMode, isMobileMode,
onSelect, onSelect,
}) => { }) => {
@@ -30,7 +30,7 @@ const RecommendOptions: React.FC<Props> = ({
const initData = async () => { const initData = async () => {
setLoading(true); setLoading(true);
const res = await queryEntities(entityId, domainId); const res = await queryEntities(entityId, modelId);
setLoading(false); setLoading(false);
setData(res.data.data); setData(res.data.data);
}; };
@@ -51,7 +51,7 @@ const RecommendOptions: React.FC<Props> = ({
<div className={`${prefixCls}-item-name-column`}> <div className={`${prefixCls}-item-name-column`}>
<Avatar <Avatar
shape="square" shape="square"
icon={<IconFont type={domainName === '艺人库' ? 'icon-geshou' : 'icon-zhuanji'} />} icon={<IconFont type={modelName === '艺人库' ? 'icon-geshou' : 'icon-zhuanji'} />}
src={record.url} src={record.url}
/> />
<div className={`${prefixCls}-entity-name`}> <div className={`${prefixCls}-entity-name`}>
@@ -64,7 +64,7 @@ const RecommendOptions: React.FC<Props> = ({
}, },
}; };
const playCntColumnIdex = domainName.includes('歌曲') const playCntColumnIdex = modelName.includes('歌曲')
? 'tme3platAvgLogYyPlayCnt' ? 'tme3platAvgLogYyPlayCnt'
: 'tme3platJsPlayCnt'; : 'tme3platJsPlayCnt';
@@ -72,7 +72,7 @@ const RecommendOptions: React.FC<Props> = ({
? [basicColumn] ? [basicColumn]
: [ : [
basicColumn, basicColumn,
domainName.includes('艺人') modelName.includes('艺人')
? { ? {
dataIndex: 'onlineSongCnt', dataIndex: 'onlineSongCnt',
key: 'onlineSongCnt', key: 'onlineSongCnt',
@@ -95,7 +95,7 @@ const RecommendOptions: React.FC<Props> = ({
dataIndex: playCntColumnIdex, dataIndex: playCntColumnIdex,
key: playCntColumnIdex, key: playCntColumnIdex,
align: 'center', align: 'center',
title: domainName.includes('歌曲') ? '近7天日均运营播放量' : '昨日结算播放量', title: modelName.includes('歌曲') ? '近7天日均运营播放量' : '昨日结算播放量',
render: (value: string) => { render: (value: string) => {
return value ? getFormattedValue(+value) : '-'; return value ? getFormattedValue(+value) : '-';
}, },

View File

@@ -26,21 +26,22 @@ const Tools: React.FC<Props> = ({
onChangeChart, onChangeChart,
}) => { }) => {
const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false); const [recommendOptionsOpen, setRecommendOptionsOpen] = useState(false);
const { queryColumns, queryResults, queryId, chatContext, queryMode } = data || {}; const { queryColumns, queryResults, queryId, chatContext, queryMode, entityInfo } = data || {};
const [score, setScore] = useState(scoreValue || 0); const [score, setScore] = useState(scoreValue || 0);
const prefixCls = `${CLS_PREFIX}-tools`; const prefixCls = `${CLS_PREFIX}-tools`;
const singleData = queryResults.length === 1;
const isMetricCard =
queryMode.includes('METRIC') &&
(singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate);
const noDashboard = const noDashboard =
(queryColumns?.length === 1 && (queryColumns?.length === 1 &&
queryColumns[0].showType === 'CATEGORY' && queryColumns[0].showType === 'CATEGORY' &&
queryResults?.length === 1) || queryResults?.length === 1) ||
(!queryMode.includes('METRIC') && !queryMode.includes('ENTITY')); (!queryMode.includes('METRIC') && !queryMode.includes('ENTITY')) ||
isMetricCard;
console.log(
'chatContext?.properties?.CONTEXT?.plugin?.name',
chatContext?.properties?.CONTEXT?.plugin?.name
);
const changeChart = () => { const changeChart = () => {
onChangeChart(); onChangeChart();
@@ -74,13 +75,13 @@ const Tools: React.FC<Props> = ({
return ( return (
<div className={prefixCls}> <div className={prefixCls}>
{/* {isLastMessage && chatContext?.domainId && entityInfo?.entityId && ( {/* {isLastMessage && chatContext?.modelId && entityInfo?.entityId && (
<Popover <Popover
content={ content={
<RecommendOptions <RecommendOptions
entityId={entityInfo.entityId} entityId={entityInfo.entityId}
domainId={chatContext.domainId} modelId={chatContext.modelId}
domainName={chatContext.domainName} modelName={chatContext.modelName}
isMobileMode={isMobileMode} isMobileMode={isMobileMode}
onSelect={switchEntity} onSelect={switchEntity}
/> />
@@ -105,7 +106,7 @@ const Tools: React.FC<Props> = ({
</Button> </Button>
)} )}
{isLastMessage && ( {isLastMessage && !isMetricCard && (
<div className={`${prefixCls}-feedback`}> <div className={`${prefixCls}-feedback`}>
<div></div> <div></div>
<LikeOutlined className={likeClass} onClick={like} /> <LikeOutlined className={likeClass} onClick={like} />

View File

@@ -56,7 +56,6 @@ const Chat = () => {
msg={msg} msg={msg}
// msgData={data} // msgData={data}
onMsgDataLoaded={onMsgDataLoaded} onMsgDataLoaded={onMsgDataLoaded}
domainId={37}
isLastMessage isLastMessage
isMobileMode isMobileMode
triggerResize={triggerResize} triggerResize={triggerResize}

View File

@@ -19,7 +19,7 @@ export { default as ChatItem } from './components/ChatItem';
export type { export type {
SearchRecommendItem, SearchRecommendItem,
FieldType, FieldType,
DomainInfoType, ModelInfoType,
EntityInfoType, EntityInfoType,
DateInfoType, DateInfoType,
ChatContextType, ChatContextType,

View File

@@ -6,30 +6,30 @@ const DEFAULT_CHAT_ID = 0;
const prefix = '/api'; const prefix = '/api';
export function searchRecommend(queryText: string, chatId?: number, domainId?: number) { export function searchRecommend(queryText: string, chatId?: number, modelId?: number) {
return axios.post<Result<SearchRecommendItem[]>>(`${prefix}/chat/query/search`, { return axios.post<Result<SearchRecommendItem[]>>(`${prefix}/chat/query/search`, {
queryText, queryText,
chatId: chatId || DEFAULT_CHAT_ID, chatId: chatId || DEFAULT_CHAT_ID,
domainId, modelId,
}); });
} }
export function chatQuery(queryText: string, chatId?: number, domainId?: number, filters?: any[]) { export function chatQuery(queryText: string, chatId?: number, modelId?: number, filters?: any[]) {
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/query`, { return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/query`, {
queryText, queryText,
chatId: chatId || DEFAULT_CHAT_ID, chatId: chatId || DEFAULT_CHAT_ID,
domainId, modelId,
queryFilters: filters ? { queryFilters: filters ? {
filters filters
} : undefined, } : undefined,
}); });
} }
export function chatParse(queryText: string, chatId?: number, domainId?: number, filters?: any[]) { export function chatParse(queryText: string, chatId?: number, modelId?: number, filters?: any[]) {
return axios.post<Result<ParseDataType>>(`${prefix}/chat/query/parse`, { return axios.post<Result<ParseDataType>>(`${prefix}/chat/query/parse`, {
queryText, queryText,
chatId: chatId || DEFAULT_CHAT_ID, chatId: chatId || DEFAULT_CHAT_ID,
domainId, modelId,
queryFilters: filters ? { queryFilters: filters ? {
filters filters
} : undefined, } : undefined,
@@ -44,10 +44,10 @@ export function chatExecute(queryText: string, chatId: number, parseInfo: ChatC
}); });
} }
export function switchEntity(entityId: string, domainId?: number, chatId?: number) { export function switchEntity(entityId: string, modelId?: number, chatId?: number) {
return axios.post<Result<any>>(`${prefix}/chat/query/switchQuery`, { return axios.post<Result<any>>(`${prefix}/chat/query/switchQuery`, {
queryText: entityId, queryText: entityId,
domainId, modelId,
chatId: chatId || DEFAULT_CHAT_ID, chatId: chatId || DEFAULT_CHAT_ID,
}); });
} }
@@ -63,8 +63,8 @@ export function queryContext(queryText: string, chatId?: number) {
}); });
} }
export function querySuggestionInfo(domainId: number) { export function querySuggestionInfo(modelId: number) {
return axios.get<Result<any>>(`${prefix}/chat/recommend/${domainId}`); return axios.get<Result<any>>(`${prefix}/chat/recommend/${modelId}`);
} }
export function getHistoryMsg(current: number, chatId: number = DEFAULT_CHAT_ID, pageSize: number = 10) { export function getHistoryMsg(current: number, chatId: number = DEFAULT_CHAT_ID, pageSize: number = 10) {
@@ -98,10 +98,10 @@ export function getAllConversations() {
return axios.get<Result<any>>(`${prefix}/chat/manage/getAll`); return axios.get<Result<any>>(`${prefix}/chat/manage/getAll`);
} }
export function queryEntities(entityId: string | number, domainId: number) { export function queryEntities(entityId: string | number, modelId: number) {
return axios.post<Result<any>>(`${prefix}/chat/query/choice`, { return axios.post<Result<any>>(`${prefix}/chat/query/choice`, {
entityId, entityId,
domainId, modelId,
}); });
} }
@@ -109,6 +109,6 @@ export function updateQAFeedback(questionId: number, score: number) {
return axios.post<Result<any>>(`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`); return axios.post<Result<any>>(`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`);
} }
export function queryDrillDownDimensions(domainId: number) { export function queryDrillDownDimensions(modelId: number) {
return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${domainId}`); return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${modelId}`);
} }

View File

@@ -13,6 +13,7 @@ import { queryToken } from './services/login';
import { queryCurrentUser } from './services/user'; import { queryCurrentUser } from './services/user';
import { traverseRoutes, deleteUrlQuery } from './utils/utils'; import { traverseRoutes, deleteUrlQuery } from './utils/utils';
import { publicPath } from '../config/defaultSettings'; import { publicPath } from '../config/defaultSettings';
import Copilot from './pages/Copilot';
export { request } from './services/request'; export { request } from './services/request';
const TOKEN_KEY = AUTH_TOKEN_KEY; const TOKEN_KEY = AUTH_TOKEN_KEY;
@@ -148,7 +149,12 @@ export const layout: RunTimeLayoutConfig = (params) => {
disableContentMargin: true, disableContentMargin: true,
menuHeaderRender: undefined, menuHeaderRender: undefined,
childrenRender: (dom) => { childrenRender: (dom) => {
return dom; return (
<>
{dom}
{history.location.pathname !== '/chat' && <Copilot />}
</>
);
}, },
openKeys: false, openKeys: false,
...initialState?.settings, ...initialState?.settings,

View File

@@ -9,21 +9,21 @@ import { searchRecommend } from 'supersonic-chat-sdk';
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants'; import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
import styles from './style.less'; import styles from './style.less';
import { PLACE_HOLDER } from '../constants'; import { PLACE_HOLDER } from '../constants';
import { DefaultEntityType, DomainType } from '../type'; import { DefaultEntityType, ModelType } from '../type';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
type Props = { type Props = {
inputMsg: string; inputMsg: string;
chatId?: number; chatId?: number;
currentDomain?: DomainType; currentModel?: ModelType;
defaultEntity?: DefaultEntityType; defaultEntity?: DefaultEntityType;
isCopilotMode?: boolean; isCopilotMode?: boolean;
copilotFullscreen?: boolean; copilotFullscreen?: boolean;
domains: DomainType[]; models: ModelType[];
collapsed: boolean; collapsed: boolean;
onToggleCollapseBtn: () => void; onToggleCollapseBtn: () => void;
onInputMsgChange: (value: string) => void; onInputMsgChange: (value: string) => void;
onSendMsg: (msg: string, domainId?: number) => void; onSendMsg: (msg: string, modelId?: number) => void;
onAddConversation: () => void; onAddConversation: () => void;
onCancelDefaultFilter: () => void; onCancelDefaultFilter: () => void;
}; };
@@ -44,9 +44,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
{ {
inputMsg, inputMsg,
chatId, chatId,
currentDomain, currentModel,
defaultEntity, defaultEntity,
domains, models,
collapsed, collapsed,
isCopilotMode, isCopilotMode,
copilotFullscreen, copilotFullscreen,
@@ -58,7 +58,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
}, },
ref, ref,
) => { ) => {
const [domainOptions, setDomainOptions] = useState<DomainType[]>([]); const [modelOptions, setModelOptions] = useState<ModelType[]>([]);
const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({}); const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({});
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
@@ -100,7 +100,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
}, []); }, []);
const getStepOptions = (recommends: any[]) => { const getStepOptions = (recommends: any[]) => {
const data = groupByColumn(recommends, 'domainName'); const data = groupByColumn(recommends, 'modelName');
return isMobile && recommends.length > 6 return isMobile && recommends.length > 6
? Object.keys(data) ? Object.keys(data)
.slice(0, 4) .slice(0, 4)
@@ -114,23 +114,23 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
: data; : data;
}; };
const processMsg = (msg: string, domains: DomainType[]) => { const processMsg = (msg: string, models: ModelType[]) => {
let msgValue = msg; let msgValue = msg;
let domainId: number | undefined; let modelId: number | undefined;
if (msg?.[0] === '@') { if (msg?.[0] === '@') {
const domain = domains.find((item) => msg.includes(`@${item.name}`)); const model = models.find((item) => msg.includes(`@${item.name}`));
msgValue = domain ? msg.replace(`@${domain.name}`, '') : msg; msgValue = model ? msg.replace(`@${model.name}`, '') : msg;
domainId = domain?.id; modelId = model?.id;
} }
return { msgValue, domainId }; return { msgValue, modelId };
}; };
const debounceGetWordsFunc = useCallback(() => { const debounceGetWordsFunc = useCallback(() => {
const getAssociateWords = async ( const getAssociateWords = async (
msg: string, msg: string,
domains: DomainType[], models: ModelType[],
chatId?: number, chatId?: number,
domain?: DomainType, model?: ModelType,
) => { ) => {
if (isPinyin) { if (isPinyin) {
return; return;
@@ -140,9 +140,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
} }
fetchRef.current += 1; fetchRef.current += 1;
const fetchId = fetchRef.current; const fetchId = fetchRef.current;
const { msgValue, domainId } = processMsg(msg, domains); const { msgValue, modelId } = processMsg(msg, models);
const domainIdValue = domainId || domain?.id; const modelIdValue = modelId || model?.id;
const res = await searchRecommend(msgValue.trim(), chatId, domainIdValue); const res = await searchRecommend(msgValue.trim(), chatId, modelIdValue);
if (fetchId !== fetchRef.current) { if (fetchId !== fetchRef.current) {
return; return;
} }
@@ -165,19 +165,19 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
useEffect(() => { useEffect(() => {
if (inputMsg.length === 1 && inputMsg[0] === '@') { if (inputMsg.length === 1 && inputMsg[0] === '@') {
setOpen(true); setOpen(true);
setDomainOptions(domains); setModelOptions(models);
setStepOptions({}); setStepOptions({});
return; return;
} else { } else {
setOpen(false); setOpen(false);
if (domainOptions.length > 0) { if (modelOptions.length > 0) {
setTimeout(() => { setTimeout(() => {
setDomainOptions([]); setModelOptions([]);
}, 500); }, 500);
} }
} }
if (!isSelect) { if (!isSelect) {
debounceGetWords(inputMsg, domains, chatId, currentDomain); debounceGetWords(inputMsg, models, chatId, currentModel);
} else { } else {
isSelect = false; isSelect = false;
} }
@@ -219,10 +219,10 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
.find((item) => .find((item) =>
Object.keys(stepOptions).length === 1 Object.keys(stepOptions).length === 1
? item.recommend === value ? item.recommend === value
: `${item.domainName || ''}${item.recommend}` === value, : `${item.modelName || ''}${item.recommend}` === value,
); );
if (option && isSelect) { if (option && isSelect) {
onSendMsg(option.recommend, option.domainId); onSendMsg(option.recommend, option.modelId);
} else { } else {
onSendMsg(value.trim()); onSendMsg(value.trim());
} }
@@ -230,12 +230,12 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, { const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
[styles.mobile]: isMobile, [styles.mobile]: isMobile,
[styles.domainOptions]: domainOptions.length > 0, [styles.modelOptions]: modelOptions.length > 0,
}); });
const onSelect = (value: string) => { const onSelect = (value: string) => {
isSelect = true; isSelect = true;
if (domainOptions.length === 0) { if (modelOptions.length === 0) {
sendMsg(value); sendMsg(value);
} }
setOpen(false); setOpen(false);
@@ -263,19 +263,16 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
/> />
</Tooltip> </Tooltip>
<div className={styles.composerInputWrapper}> <div className={styles.composerInputWrapper}>
{currentDomain && ( {currentModel && (
<div className={styles.currentDomain}> <div className={styles.currentModel}>
<div className={styles.currentDomainName}> <div className={styles.currentModelName}>
<span className={styles.quoteText}> <span className={styles.quoteText}>
{currentDomain.name} {currentModel.name}
{defaultEntity && ( {defaultEntity && (
<> <>
<span></span> <span></span>
<span>{`${currentDomain.name.slice( <span>{`${currentModel.name.slice(0, currentModel.name.length - 1)}`}</span>
0,
currentDomain.name.length - 1,
)}`}</span>
<span className={styles.entityName} title={defaultEntity.entityName}> <span className={styles.entityName} title={defaultEntity.entityName}>
{defaultEntity.entityName} {defaultEntity.entityName}
</span> </span>
@@ -285,7 +282,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
</span> </span>
</div> </div>
<div className={styles.cancelDomain} onClick={onCancelDefaultFilter}> <div className={styles.cancelModel} onClick={onCancelDefaultFilter}>
</div> </div>
</div> </div>
@@ -293,8 +290,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
<AutoComplete <AutoComplete
className={styles.composerInput} className={styles.composerInput}
placeholder={ placeholder={
currentDomain currentModel
? `请输入【${currentDomain.name}】主题的问题,可使用@切换到其他主题` ? `请输入【${currentModel.name}】主题的问题,可使用@切换到其他主题`
: PLACE_HOLDER : PLACE_HOLDER
} }
value={inputMsg} value={inputMsg}
@@ -323,15 +320,15 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
open={open} open={open}
getPopupContainer={(triggerNode) => triggerNode.parentNode} getPopupContainer={(triggerNode) => triggerNode.parentNode}
> >
{domainOptions.length > 0 {modelOptions.length > 0
? domainOptions.map((domain) => { ? modelOptions.map((model) => {
return ( return (
<Option <Option
key={domain.id} key={model.id}
value={`@${domain.name} `} value={`@${model.name} `}
className={styles.searchOption} className={styles.searchOption}
> >
{domain.name} {model.name}
</Option> </Option>
); );
}) })
@@ -342,17 +339,15 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
let optionValue = let optionValue =
Object.keys(stepOptions).length === 1 Object.keys(stepOptions).length === 1
? option.recommend ? option.recommend
: `${option.domainName || ''}${option.recommend}`; : `${option.modelName || ''}${option.recommend}`;
if (inputMsg[0] === '@') { if (inputMsg[0] === '@') {
const domain = domains.find((item) => inputMsg.includes(item.name)); const model = models.find((item) => inputMsg.includes(item.name));
optionValue = domain optionValue = model ? `@${model.name} ${option.recommend}` : optionValue;
? `@${domain.name} ${option.recommend}`
: optionValue;
} }
return ( return (
<Option <Option
key={`${option.recommend}${ key={`${option.recommend}${
option.domainName ? `_${option.domainName}` : '' option.modelName ? `_${option.modelName}` : ''
}`} }`}
value={optionValue} value={optionValue}
className={styles.searchOption} className={styles.searchOption}
@@ -363,7 +358,7 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
className={styles.semanticType} className={styles.semanticType}
color={ color={
option.schemaElementType === SemanticTypeEnum.DIMENSION || option.schemaElementType === SemanticTypeEnum.DIMENSION ||
option.schemaElementType === SemanticTypeEnum.DOMAIN option.schemaElementType === SemanticTypeEnum.MODEL
? 'blue' ? 'blue'
: option.schemaElementType === SemanticTypeEnum.VALUE : option.schemaElementType === SemanticTypeEnum.VALUE
? 'geekblue' ? 'geekblue'

View File

@@ -45,7 +45,7 @@
position: relative; position: relative;
flex: 1; flex: 1;
.currentDomain { .currentModel {
position: absolute; position: absolute;
top: -30px; top: -30px;
left: 15px; left: 15px;
@@ -61,7 +61,7 @@
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-top-right-radius: 6px; border-top-right-radius: 6px;
.currentDomainName { .currentModelName {
margin-right: 12px; margin-right: 12px;
font-size: 14px; font-size: 14px;
@@ -75,7 +75,7 @@
} }
} }
.cancelDomain { .cancelModel {
padding: 0 6px; padding: 0 6px;
font-size: 13px; font-size: 13px;
border: 1px solid var(--text-color-fourth); border: 1px solid var(--text-color-fourth);
@@ -206,7 +206,7 @@
} }
} }
.domain { .model {
margin-top: 2px; margin-top: 2px;
color: var(--text-color-fourth); color: var(--text-color-fourth);
font-size: 13px; font-size: 13px;
@@ -219,7 +219,7 @@
min-width: 100px !important; min-width: 100px !important;
border-radius: 6px; border-radius: 6px;
&.domainOptions { &.modelOptions {
width: 150px !important; width: 150px !important;
.searchOption { .searchOption {

View File

@@ -21,14 +21,14 @@ type Props = {
currentConversation?: ConversationDetailType; currentConversation?: ConversationDetailType;
collapsed?: boolean; collapsed?: boolean;
isCopilotMode?: boolean; isCopilotMode?: boolean;
defaultDomainName?: string; defaultModelName?: string;
defaultEntityFilter?: DefaultEntityType; defaultEntityFilter?: DefaultEntityType;
triggerNewConversation?: boolean; triggerNewConversation?: boolean;
onNewConversationTriggered?: () => void; onNewConversationTriggered?: () => void;
onSelectConversation: ( onSelectConversation: (
conversation: ConversationDetailType, conversation: ConversationDetailType,
name?: string, name?: string,
domainId?: number, modelId?: number,
entityId?: string, entityId?: string,
) => void; ) => void;
}; };
@@ -38,7 +38,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
currentConversation, currentConversation,
collapsed, collapsed,
isCopilotMode, isCopilotMode,
defaultDomainName, defaultModelName,
defaultEntityFilter, defaultEntityFilter,
triggerNewConversation, triggerNewConversation,
onNewConversationTriggered, onNewConversationTriggered,
@@ -47,7 +47,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
ref, ref,
) => { ) => {
const location = useLocation(); const location = useLocation();
const { q, cid, domainId, entityId } = (location as any).query; const { q, cid, modelId, entityId } = (location as any).query;
const [conversations, setConversations] = useState<ConversationDetailType[]>([]); const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
const [editModalVisible, setEditModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false);
const [editConversation, setEditConversation] = useState<ConversationDetailType>(); const [editConversation, setEditConversation] = useState<ConversationDetailType>();
@@ -89,7 +89,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
const conversationName = const conversationName =
defaultEntityFilter?.entityName && window.location.pathname.includes('detail') defaultEntityFilter?.entityName && window.location.pathname.includes('detail')
? defaultEntityFilter.entityName ? defaultEntityFilter.entityName
: defaultDomainName; : defaultModelName;
onAddConversation({ name: conversationName, type: 'CUSTOMIZE' }); onAddConversation({ name: conversationName, type: 'CUSTOMIZE' });
onNewConversationTriggered?.(); onNewConversationTriggered?.();
} }
@@ -100,7 +100,7 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
return; return;
} }
if (q && cid === undefined && window.location.href.includes('/workbench/chat')) { if (q && cid === undefined && window.location.href.includes('/workbench/chat')) {
onAddConversation({ name: q, domainId: domainId ? +domainId : undefined, entityId }); onAddConversation({ name: q, modelId: modelId ? +modelId : undefined, entityId });
} else { } else {
initData(); initData();
} }
@@ -118,17 +118,17 @@ const Conversation: ForwardRefRenderFunction<any, Props> = (
const onAddConversation = async ({ const onAddConversation = async ({
name, name,
domainId, modelId,
entityId, entityId,
type, type,
}: { }: {
name?: string; name?: string;
domainId?: number; modelId?: number;
entityId?: string; entityId?: string;
type?: string; type?: string;
} = {}) => { } = {}) => {
const data = await addConversation(name); const data = await addConversation(name);
onSelectConversation(data[0], type || name, domainId, entityId); onSelectConversation(data[0], type || name, modelId, entityId);
}; };
const onOperate = (key: string, conversation: ConversationDetailType) => { const onOperate = (key: string, conversation: ConversationDetailType) => {

View File

@@ -23,7 +23,7 @@ type Props = {
valid: boolean, valid: boolean,
) => void; ) => void;
onCheckMore: (data: MsgDataType) => void; onCheckMore: (data: MsgDataType) => void;
onApplyAuth: (domain: string) => void; onApplyAuth: (model: string) => void;
}; };
const MessageContainer: React.FC<Props> = ({ const MessageContainer: React.FC<Props> = ({
@@ -71,15 +71,15 @@ const MessageContainer: React.FC<Props> = ({
for (let i = 0; i < msgs.length; i++) { for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i]; const msg = msgs[i];
const msgDomainId = msg.msgData?.chatContext?.domainId; const msgModelId = msg.msgData?.chatContext?.modelId;
const msgEntityId = msg.msgData?.entityInfo?.entityId; const msgEntityId = msg.msgData?.entityInfo?.entityId;
const currentMsgDomainId = currentMsgData?.chatContext?.domainId; const currentMsgModelId = currentMsgData?.chatContext?.modelId;
const currentMsgEntityId = currentMsgData?.entityInfo?.entityId; const currentMsgEntityId = currentMsgData?.entityInfo?.entityId;
if ( if (
(msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.PLUGIN) && (msg.type === MessageTypeEnum.QUESTION || msg.type === MessageTypeEnum.PLUGIN) &&
!!currentMsgDomainId && !!currentMsgModelId &&
msgDomainId === currentMsgDomainId && msgModelId === currentMsgModelId &&
msgEntityId === currentMsgEntityId && msgEntityId === currentMsgEntityId &&
msg.msg msg.msg
) { ) {
@@ -91,8 +91,8 @@ const MessageContainer: React.FC<Props> = ({
return followQuestions; return followQuestions;
}; };
const getFilters = (domainId?: number, entityId?: string) => { const getFilters = (modelId?: number, entityId?: string) => {
if (!domainId || !entityId) { if (!modelId || !entityId) {
return undefined; return undefined;
} }
return [ return [
@@ -108,7 +108,7 @@ const MessageContainer: React.FC<Props> = ({
{messageList.map((msgItem: MessageItem, index: number) => { {messageList.map((msgItem: MessageItem, index: number) => {
const { const {
id: msgId, id: msgId,
domainId, modelId,
entityId, entityId,
type, type,
msg, msg,
@@ -117,6 +117,7 @@ const MessageContainer: React.FC<Props> = ({
msgData, msgData,
score, score,
isHistory, isHistory,
parseOptions,
} = msgItem; } = msgItem;
const followQuestions = getFollowQuestions(index); const followQuestions = getFollowQuestions(index);
@@ -132,8 +133,8 @@ const MessageContainer: React.FC<Props> = ({
msg={msgValue || msg || ''} msg={msgValue || msg || ''}
msgData={msgData} msgData={msgData}
conversationId={chatId} conversationId={chatId}
domainId={domainId} modelId={modelId}
filter={getFilters(domainId, entityId)} filter={getFilters(modelId, entityId)}
isLastMessage={index === messageList.length - 1} isLastMessage={index === messageList.length - 1}
isMobileMode={isMobileMode} isMobileMode={isMobileMode}
triggerResize={triggerResize} triggerResize={triggerResize}
@@ -144,6 +145,22 @@ const MessageContainer: React.FC<Props> = ({
/> />
</> </>
)} )}
{type === MessageTypeEnum.PARSE_OPTIONS && (
<ChatItem
msg={msgValue || msg || ''}
conversationId={chatId}
modelId={modelId}
filter={getFilters(modelId, entityId)}
isLastMessage={index === messageList.length - 1}
isMobileMode={isMobileMode}
triggerResize={triggerResize}
parseOptions={parseOptions}
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
}}
onUpdateMessageScroll={updateMessageContainerScroll}
/>
)}
{type === MessageTypeEnum.PLUGIN && ( {type === MessageTypeEnum.PLUGIN && (
<> <>
<Plugin <Plugin

View File

@@ -6,42 +6,16 @@ type Props = {
width?: number | string; width?: number | string;
height?: number | string; height?: number | string;
bubbleClassName?: string; bubbleClassName?: string;
domainName?: string;
question?: string;
followQuestions?: string[];
}; };
const Message: React.FC<Props> = ({ const Message: React.FC<Props> = ({ position, width, height, children, bubbleClassName }) => {
position,
width,
height,
children,
bubbleClassName,
domainName,
question,
followQuestions,
}) => {
const messageClass = classNames(styles.message, { const messageClass = classNames(styles.message, {
[styles.left]: position === 'left', [styles.left]: position === 'left',
[styles.right]: position === 'right', [styles.right]: position === 'right',
}); });
const leftTitle = question
? followQuestions && followQuestions.length > 0
? `多轮对话:${[question, ...followQuestions].join(' ← ')}`
: `单轮对话:${question}`
: '';
return ( return (
<div className={messageClass} style={{ width }}> <div className={messageClass} style={{ width }}>
{/* <div className={styles.messageTitleBar}>
{!!domainName && <div className={styles.domainName}>{domainName}</div>}
{position === 'left' && leftTitle && (
<div className={styles.messageTopBar} title={leftTitle}>
({leftTitle})
</div>
)}
</div> */}
<div className={styles.messageContent}> <div className={styles.messageContent}>
<div className={styles.messageBody}> <div className={styles.messageBody}>
<div <div
@@ -51,11 +25,6 @@ const Message: React.FC<Props> = ({
e.stopPropagation(); e.stopPropagation();
}} }}
> >
{/* {position === 'left' && question && (
<div className={styles.messageTopBar} title={leftTitle}>
{leftTitle}
</div>
)} */}
{children} {children}
</div> </div>
</div> </div>

View File

@@ -161,15 +161,7 @@ const Plugin: React.FC<Props> = ({
<div className={reportClass}> <div className={reportClass}>
<LeftAvatar /> <LeftAvatar />
<div className={styles.msgContent}> <div className={styles.msgContent}>
<Message <Message position="left" width="100%" height={height} bubbleClassName={styles.reportBubble}>
position="left"
width="100%"
height={height}
bubbleClassName={styles.reportBubble}
domainName={data.chatContext?.domainName}
question={msg}
followQuestions={followQuestions}
>
<iframe <iframe
id={`reportIframe_${id}`} id={`reportIframe_${id}`}
src={pluginUrl} src={pluginUrl}

View File

@@ -5,7 +5,7 @@
margin-bottom: 6px; margin-bottom: 6px;
column-gap: 10px; column-gap: 10px;
.domainName { .modelName {
margin-left: 4px; margin-left: 4px;
color: var(--text-color); color: var(--text-color);
font-weight: 500; font-weight: 500;

View File

@@ -14,14 +14,14 @@ export const THEME_COLOR_LIST = [
]; ];
export enum SemanticTypeEnum { export enum SemanticTypeEnum {
DOMAIN = 'DOMAIN', MODEL = 'MODEL',
DIMENSION = 'DIMENSION', DIMENSION = 'DIMENSION',
METRIC = 'METRIC', METRIC = 'METRIC',
VALUE = 'VALUE', VALUE = 'VALUE',
} }
export const SEMANTIC_TYPE_MAP = { export const SEMANTIC_TYPE_MAP = {
[SemanticTypeEnum.DOMAIN]: '主题域', [SemanticTypeEnum.MODEL]: '主题域',
[SemanticTypeEnum.DIMENSION]: '维度', [SemanticTypeEnum.DIMENSION]: '维度',
[SemanticTypeEnum.METRIC]: '指标', [SemanticTypeEnum.METRIC]: '指标',
[SemanticTypeEnum.VALUE]: '维度值', [SemanticTypeEnum.VALUE]: '维度值',

View File

@@ -6,11 +6,11 @@ import styles from './style.less';
import { import {
ConversationDetailType, ConversationDetailType,
DefaultEntityType, DefaultEntityType,
DomainType, ModelType,
MessageItem, MessageItem,
MessageTypeEnum, MessageTypeEnum,
} from './type'; } from './type';
import { getDomainList } from './service'; import { getModelList } from './service';
import { useThrottleFn } from 'ahooks'; import { useThrottleFn } from 'ahooks';
import Conversation from './Conversation'; import Conversation from './Conversation';
import ChatFooter from './ChatFooter'; import ChatFooter from './ChatFooter';
@@ -25,12 +25,12 @@ import { AUTH_TOKEN_KEY } from '@/common/constants';
type Props = { type Props = {
isCopilotMode?: boolean; isCopilotMode?: boolean;
copilotFullscreen?: boolean; copilotFullscreen?: boolean;
defaultDomainName?: string; defaultModelName?: string;
defaultEntityFilter?: DefaultEntityType; defaultEntityFilter?: DefaultEntityType;
copilotSendMsg?: string; copilotSendMsg?: string;
triggerNewConversation?: boolean; triggerNewConversation?: boolean;
onNewConversationTriggered?: () => void; onNewConversationTriggered?: () => void;
onCurrentDomainChange?: (domain?: DomainType) => void; onCurrentModelChange?: (model?: ModelType) => void;
onCancelCopilotFilter?: () => void; onCancelCopilotFilter?: () => void;
onCheckMoreDetail?: () => void; onCheckMoreDetail?: () => void;
}; };
@@ -38,17 +38,16 @@ type Props = {
const Chat: React.FC<Props> = ({ const Chat: React.FC<Props> = ({
isCopilotMode, isCopilotMode,
copilotFullscreen, copilotFullscreen,
defaultDomainName, defaultModelName,
defaultEntityFilter, defaultEntityFilter,
copilotSendMsg, copilotSendMsg,
triggerNewConversation, triggerNewConversation,
onNewConversationTriggered, onNewConversationTriggered,
onCurrentDomainChange, onCurrentModelChange,
onCancelCopilotFilter, onCancelCopilotFilter,
onCheckMoreDetail, onCheckMoreDetail,
}) => { }) => {
const isMobileMode = isMobile || isCopilotMode; const isMobileMode = isMobile || isCopilotMode;
const localConversationCollapsed = localStorage.getItem('CONVERSATION_COLLAPSED');
const [messageList, setMessageList] = useState<MessageItem[]>([]); const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [inputMsg, setInputMsg] = useState(''); const [inputMsg, setInputMsg] = useState('');
@@ -58,54 +57,52 @@ const Chat: React.FC<Props> = ({
const [currentConversation, setCurrentConversation] = useState< const [currentConversation, setCurrentConversation] = useState<
ConversationDetailType | undefined ConversationDetailType | undefined
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined); >(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
const [conversationCollapsed, setConversationCollapsed] = useState( const [conversationCollapsed, setConversationCollapsed] = useState(isCopilotMode);
!localConversationCollapsed ? true : localConversationCollapsed === 'true', const [models, setModels] = useState<ModelType[]>([]);
); const [currentModel, setCurrentModel] = useState<ModelType>();
const [domains, setDomains] = useState<DomainType[]>([]);
const [currentDomain, setCurrentDomain] = useState<DomainType>();
const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>(); const [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
const [applyAuthVisible, setApplyAuthVisible] = useState(false); const [applyAuthVisible, setApplyAuthVisible] = useState(false);
const [applyAuthDomain, setApplyAuthDomain] = useState(''); const [applyAuthModel, setApplyAuthModel] = useState('');
const [initialDomainName, setInitialDomainName] = useState(''); const [initialModelName, setInitialModelName] = useState('');
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { domainName } = (location as any).query; const { modelName } = (location as any).query;
const conversationRef = useRef<any>(); const conversationRef = useRef<any>();
const chatFooterRef = useRef<any>(); const chatFooterRef = useRef<any>();
useEffect(() => { useEffect(() => {
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || ''); setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
initDomains(); initModels();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (domains.length > 0 && initialDomainName && !currentDomain) { if (models.length > 0 && initialModelName && !currentModel) {
changeDomain(domains.find((domain) => domain.name === initialDomainName)); changeModel(models.find((model) => model.name === initialModelName));
} }
}, [domains]); }, [models]);
useEffect(() => { useEffect(() => {
if (domainName) { if (modelName) {
setInitialDomainName(domainName); setInitialModelName(modelName);
} }
}, [domainName]); }, [modelName]);
useEffect(() => { useEffect(() => {
if (defaultDomainName !== undefined && domains.length > 0) { if (defaultModelName !== undefined && models.length > 0) {
changeDomain(domains.find((domain) => domain.name === defaultDomainName)); changeModel(models.find((model) => model.name === defaultModelName));
} }
}, [defaultDomainName]); }, [defaultModelName]);
useEffect(() => { useEffect(() => {
if (!currentConversation) { if (!currentConversation) {
return; return;
} }
const { initMsg, domainId, entityId } = currentConversation; const { initMsg, modelId, entityId } = currentConversation;
if (initMsg) { if (initMsg) {
inputFocus(); inputFocus();
if (initMsg === 'CUSTOMIZE' && copilotSendMsg) { if (initMsg === 'CUSTOMIZE' && copilotSendMsg) {
onSendMsg(copilotSendMsg, [], domainId, entityId); onSendMsg(copilotSendMsg, [], modelId, entityId);
dispatch({ dispatch({
type: 'globalState/setCopilotSendMsg', type: 'globalState/setCopilotSendMsg',
payload: '', payload: '',
@@ -116,7 +113,7 @@ const Chat: React.FC<Props> = ({
sendHelloRsp(); sendHelloRsp();
return; return;
} }
onSendMsg(initMsg, [], domainId, entityId); onSendMsg(initMsg, [], modelId, entityId);
return; return;
} }
updateHistoryMsg(1); updateHistoryMsg(1);
@@ -147,13 +144,13 @@ const Chat: React.FC<Props> = ({
{ {
id: uuid(), id: uuid(),
type: MessageTypeEnum.TEXT, type: MessageTypeEnum.TEXT,
msg: defaultDomainName msg: defaultModelName
? `您好,请输入关于${ ? `您好,请输入关于${
defaultEntityFilter?.entityName defaultEntityFilter?.entityName
? `${defaultDomainName?.slice(0, defaultDomainName?.length - 1)}${ ? `${defaultModelName?.slice(0, defaultModelName?.length - 1)}${
defaultEntityFilter?.entityName defaultEntityFilter?.entityName
}` }`
: `${defaultDomainName}` : `${defaultModelName}`
}的问题` }的问题`
: '您好,请问有什么我能帮您吗?', : '您好,请问有什么我能帮您吗?',
}, },
@@ -208,19 +205,19 @@ const Chat: React.FC<Props> = ({
}, },
); );
const changeDomain = (domain?: DomainType) => { const changeModel = (model?: ModelType) => {
setCurrentDomain(domain); setCurrentModel(model);
if (onCurrentDomainChange) { if (onCurrentModelChange) {
onCurrentDomainChange(domain); onCurrentModelChange(model);
} }
}; };
const initDomains = async () => { const initModels = async () => {
const res = await getDomainList(); const res = await getModelList();
const domainList = getLeafList(res.data); const modelList = getLeafList(res.data);
setDomains([{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...domainList].slice(0, 11)); setModels([{ id: -1, name: '全部', bizName: 'all', parentId: 0 }, ...modelList].slice(0, 11));
if (defaultDomainName !== undefined) { if (defaultModelName !== undefined) {
changeDomain(domainList.find((domain) => domain.name === defaultDomainName)); changeModel(modelList.find((model) => model.name === defaultModelName));
} }
}; };
@@ -237,7 +234,7 @@ const Chat: React.FC<Props> = ({
const onSendMsg = async ( const onSendMsg = async (
msg?: string, msg?: string,
list?: MessageItem[], list?: MessageItem[],
domainId?: number, modelId?: number,
entityId?: string, entityId?: string,
) => { ) => {
const currentMsg = msg || inputMsg; const currentMsg = msg || inputMsg;
@@ -245,25 +242,25 @@ const Chat: React.FC<Props> = ({
setInputMsg(''); setInputMsg('');
return; return;
} }
const msgDomain = domains.find((item) => currentMsg.includes(item.name)); const msgModel = models.find((item) => currentMsg.includes(item.name));
const certainDomain = currentMsg[0] === '@' && msgDomain; const certainModel = currentMsg[0] === '@' && msgModel;
let domainChanged = false; let modelChanged = false;
if (certainDomain) { if (certainModel) {
const toDomain = msgDomain.id === -1 ? undefined : msgDomain; const toModel = msgModel.id === -1 ? undefined : msgModel;
changeDomain(toDomain); changeModel(toModel);
domainChanged = currentDomain?.id !== toDomain?.id; modelChanged = currentModel?.id !== toModel?.id;
} }
const domainIdValue = domainId || msgDomain?.id || currentDomain?.id; const modelIdValue = modelId || msgModel?.id || currentModel?.id;
const msgs = [ const msgs = [
...(list || messageList), ...(list || messageList),
{ {
id: uuid(), id: uuid(),
msg: currentMsg, msg: currentMsg,
msgValue: certainDomain ? currentMsg.replace(`@${msgDomain.name}`, '').trim() : currentMsg, msgValue: certainModel ? currentMsg.replace(`@${msgModel.name}`, '').trim() : currentMsg,
domainId: domainIdValue === -1 ? undefined : domainIdValue, modelId: modelIdValue === -1 ? undefined : modelIdValue,
entityId: entityId || (domainChanged ? undefined : defaultEntity?.entityId), entityId: entityId || (modelChanged ? undefined : defaultEntity?.entityId),
identityMsg: certainDomain ? getIdentityMsgText(msgDomain) : undefined, identityMsg: certainModel ? getIdentityMsgText(msgModel) : undefined,
type: MessageTypeEnum.QUESTION, type: MessageTypeEnum.QUESTION,
}, },
]; ];
@@ -290,7 +287,7 @@ const Chat: React.FC<Props> = ({
const onSelectConversation = ( const onSelectConversation = (
conversation: ConversationDetailType, conversation: ConversationDetailType,
name?: string, name?: string,
domainId?: number, modelId?: number,
entityId?: string, entityId?: string,
) => { ) => {
if (!isMobileMode) { if (!isMobileMode) {
@@ -299,12 +296,31 @@ const Chat: React.FC<Props> = ({
setCurrentConversation({ setCurrentConversation({
...conversation, ...conversation,
initMsg: name, initMsg: name,
domainId, modelId,
entityId, entityId,
}); });
saveConversationToLocal(conversation); 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 onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => { const onMsgDataLoaded = (data: MsgDataType, questionId: string | number) => {
if (!isMobile) { if (!isMobile) {
conversationRef?.current?.updateData(); conversationRef?.current?.updateData();
@@ -312,6 +328,15 @@ const Chat: React.FC<Props> = ({
if (!data) { if (!data) {
return; return;
} }
let parseOptionsItem: any;
if (data.parseOptions && data.parseOptions.length > 0) {
parseOptionsItem = {
id: uuid(),
msg: messageList[messageList.length - 1]?.msg,
type: MessageTypeEnum.PARSE_OPTIONS,
parseOptions: data.parseOptions,
};
}
if (data.queryMode === 'WEB_PAGE') { if (data.queryMode === 'WEB_PAGE') {
setMessageList([ setMessageList([
...messageList, ...messageList,
@@ -321,16 +346,19 @@ const Chat: React.FC<Props> = ({
type: MessageTypeEnum.PLUGIN, type: MessageTypeEnum.PLUGIN,
msgData: data, msgData: data,
}, },
...(parseOptionsItem ? [parseOptionsItem] : []),
]); ]);
} else { } else {
const msgs = cloneDeep(messageList); const msgs = cloneDeep(messageList);
const msg = msgs.find((item) => item.id === questionId); const msg = msgs.find((item) => item.id === questionId);
if (msg) { if (msg) {
msg.msgData = data; msg.msgData = data;
setMessageList(msgs); setMessageList([...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])]);
} }
updateMessageContainerScroll(); updateMessageContainerScroll();
} }
updateChatFilter(data);
}; };
const onCheckMore = (data: MsgDataType) => { const onCheckMore = (data: MsgDataType) => {
@@ -354,14 +382,14 @@ const Chat: React.FC<Props> = ({
localStorage.setItem('CONVERSATION_COLLAPSED', `${!conversationCollapsed}`); localStorage.setItem('CONVERSATION_COLLAPSED', `${!conversationCollapsed}`);
}; };
const getIdentityMsgText = (domain?: DomainType) => { const getIdentityMsgText = (model?: ModelType) => {
return domain return model
? `您好,我当前身份是【${domain.name}】主题专家,我将尽力帮您解答相关问题~` ? `您好,我当前身份是【${model.name}】主题专家,我将尽力帮您解答相关问题~`
: '您好,我将尽力帮您解答所有主题相关问题~'; : '您好,我将尽力帮您解答所有主题相关问题~';
}; };
const onApplyAuth = (domain: string) => { const onApplyAuth = (model: string) => {
setApplyAuthDomain(domain); setApplyAuthModel(model);
setApplyAuthVisible(true); setApplyAuthVisible(true);
}; };
@@ -385,7 +413,7 @@ const Chat: React.FC<Props> = ({
currentConversation={currentConversation} currentConversation={currentConversation}
collapsed={conversationCollapsed} collapsed={conversationCollapsed}
isCopilotMode={isCopilotMode} isCopilotMode={isCopilotMode}
defaultDomainName={defaultDomainName} defaultModelName={defaultModelName}
defaultEntityFilter={defaultEntityFilter} defaultEntityFilter={defaultEntityFilter}
triggerNewConversation={triggerNewConversation} triggerNewConversation={triggerNewConversation}
onNewConversationTriggered={onNewConversationTriggered} onNewConversationTriggered={onNewConversationTriggered}
@@ -411,23 +439,24 @@ const Chat: React.FC<Props> = ({
<ChatFooter <ChatFooter
inputMsg={inputMsg} inputMsg={inputMsg}
chatId={currentConversation?.chatId} chatId={currentConversation?.chatId}
domains={domains} models={models}
currentDomain={currentDomain} currentModel={currentModel}
defaultEntity={defaultEntity} defaultEntity={defaultEntity}
collapsed={conversationCollapsed} collapsed={conversationCollapsed}
isCopilotMode={isCopilotMode} isCopilotMode={isCopilotMode}
copilotFullscreen={copilotFullscreen} copilotFullscreen={copilotFullscreen}
onToggleCollapseBtn={onToggleCollapseBtn} onToggleCollapseBtn={onToggleCollapseBtn}
onInputMsgChange={onInputMsgChange} onInputMsgChange={onInputMsgChange}
onSendMsg={(msg: string, domainId?: number) => { onSendMsg={(msg: string, modelId?: number) => {
onSendMsg(msg, messageList, domainId); onSendMsg(msg, messageList, modelId);
if (isMobile) { if (isMobile) {
inputBlur(); inputBlur();
} }
}} }}
onAddConversation={onAddConversation} onAddConversation={onAddConversation}
onCancelDefaultFilter={() => { onCancelDefaultFilter={() => {
changeDomain(undefined); changeModel(undefined);
setDefaultEntity(undefined);
if (onCancelCopilotFilter) { if (onCancelCopilotFilter) {
onCancelCopilotFilter(); onCancelCopilotFilter();
} }

View File

@@ -1,5 +1,5 @@
import { request } from 'umi'; import { request } from 'umi';
import { DomainType } from './type'; import { ModelType } from './type';
const prefix = '/api'; const prefix = '/api';
@@ -24,9 +24,9 @@ export function getAllConversations() {
return request<Result<any>>(`${prefix}/chat/manage/getAll`); return request<Result<any>>(`${prefix}/chat/manage/getAll`);
} }
export function getMiniProgramList(entityId: string, domainId: number) { export function getMiniProgramList(entityId: string, modelId: number) {
return request<Result<any>>( return request<Result<any>>(
`${prefix}/chat/plugin/extend/getAvailablePlugin/${entityId}/${domainId}`, `${prefix}/chat/plugin/extend/getAvailablePlugin/${entityId}/${modelId}`,
{ {
method: 'GET', method: 'GET',
skipErrorHandler: true, skipErrorHandler: true,
@@ -34,8 +34,8 @@ export function getMiniProgramList(entityId: string, domainId: number) {
); );
} }
export function getDomainList() { export function getModelList() {
return request<Result<DomainType[]>>(`${prefix}/chat/conf/domainList/view`, { return request<Result<ModelType[]>>(`${prefix}/chat/conf/modelList/view`, {
method: 'GET', method: 'GET',
}); });
} }
@@ -49,14 +49,14 @@ export function updateQAFeedback(questionId: number, score: number) {
); );
} }
export function queryMetricSuggestion(domainId: number) { export function queryMetricSuggestion(modelId: number) {
return request<Result<any>>(`${prefix}/chat/recommend/metric/${domainId}`, { return request<Result<any>>(`${prefix}/chat/recommend/metric/${modelId}`, {
method: 'GET', method: 'GET',
}); });
} }
export function querySuggestion(domainId: number) { export function querySuggestion(modelId: number) {
return request<Result<any>>(`${prefix}/chat/recommend/${domainId}`, { return request<Result<any>>(`${prefix}/chat/recommend/${modelId}`, {
method: 'GET', method: 'GET',
}); });
} }

View File

@@ -444,9 +444,9 @@
color: var(--primary-color); color: var(--primary-color);
} }
.messageItem { // .messageItem {
margin-top: 12px; // margin-top: 12px;
} // }
.messageTime { .messageTime {
display: flex; display: flex;

View File

@@ -1,4 +1,4 @@
import { MsgDataType } from 'supersonic-chat-sdk'; import { ChatContextType, MsgDataType } from 'supersonic-chat-sdk';
export enum MessageTypeEnum { export enum MessageTypeEnum {
TEXT = 'text', // 指标文本 TEXT = 'text', // 指标文本
@@ -10,6 +10,7 @@ export enum MessageTypeEnum {
PLUGIN = 'PLUGIN', // 插件 PLUGIN = 'PLUGIN', // 插件
WEB_PAGE = 'WEB_PAGE', // 插件 WEB_PAGE = 'WEB_PAGE', // 插件
RECOMMEND_QUESTIONS = 'recommend_questions', // 推荐问题 RECOMMEND_QUESTIONS = 'recommend_questions', // 推荐问题
PARSE_OPTIONS = 'parse_options', // 解析选项
} }
export type MessageItem = { export type MessageItem = {
@@ -18,13 +19,14 @@ export type MessageItem = {
msg?: string; msg?: string;
msgValue?: string; msgValue?: string;
identityMsg?: string; identityMsg?: string;
domainId?: number; modelId?: number;
entityId?: string; entityId?: string;
msgData?: MsgDataType; msgData?: MsgDataType;
quote?: string; quote?: string;
score?: number; score?: number;
feedback?: string; feedback?: string;
isHistory?: boolean; isHistory?: boolean;
parseOptions?: ChatContextType[];
}; };
export type ConversationDetailType = { export type ConversationDetailType = {
@@ -35,7 +37,7 @@ export type ConversationDetailType = {
lastQuestion?: string; lastQuestion?: string;
lastTime?: string; lastTime?: string;
initMsg?: string; initMsg?: string;
domainId?: number; modelId?: number;
entityId?: string; entityId?: string;
}; };
@@ -43,7 +45,7 @@ export enum MessageModeEnum {
INTERPRET = 'interpret', INTERPRET = 'interpret',
} }
export type DomainType = { export type ModelType = {
id: number; id: number;
parentId: number; parentId: number;
name: string; name: string;
@@ -66,12 +68,12 @@ export type PluginType = {
export type DefaultEntityType = { export type DefaultEntityType = {
entityId: string; entityId: string;
entityName: string; entityName: string;
domainName?: string; modelName?: string;
}; };
export type SuggestionItemType = { export type SuggestionItemType = {
id: number; id: number;
domain: number; model: number;
name: string; name: string;
bizName: string; bizName: string;
}; };

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Select, Form, Input, InputNumber, message, Button, Radio } from 'antd'; import { Modal, Select, Form, Input, InputNumber, message, Button, Radio } from 'antd';
import { getDimensionList, getDomainList, savePlugin } from './service'; import { getDimensionList, getModelList, savePlugin } from './service';
import { import {
DimensionType, DimensionType,
DomainType, ModelType,
ParamTypeEnum, ParamTypeEnum,
ParseModeEnum, ParseModeEnum,
PluginType, PluginType,
@@ -26,10 +26,8 @@ type Props = {
}; };
const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => { const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
const [domainList, setDomainList] = useState<DomainType[]>([]); const [modelList, setModelList] = useState<ModelType[]>([]);
const [domainDimensionList, setDomainDimensionList] = useState<Record<number, DimensionType[]>>( const [modelDimensionList, setModelDimensionList] = useState<Record<number, DimensionType[]>>({});
{},
);
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const [pluginType, setPluginType] = useState<PluginTypeEnum>(); const [pluginType, setPluginType] = useState<PluginTypeEnum>();
const [functionName, setFunctionName] = useState<string>(); const [functionName, setFunctionName] = useState<string>();
@@ -38,28 +36,25 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
const [filters, setFilters] = useState<any[]>([]); const [filters, setFilters] = useState<any[]>([]);
const [form] = Form.useForm(); const [form] = Form.useForm();
const initDomainList = async () => { const initModelList = async () => {
const res = await getDomainList(); const res = await getModelList();
setDomainList([{ id: -1, name: '全部' }, ...getLeafList(res.data)]); setModelList([{ id: -1, name: '默认' }, ...getLeafList(res.data)]);
}; };
useEffect(() => { useEffect(() => {
initDomainList(); initModelList();
}, []); }, []);
const initDomainDimensions = async (params: any) => { const initModelDimensions = async (params: any) => {
const domainIds = params const modelIds = params
.filter((param: any) => !!param.domainId) .filter((param: any) => !!param.modelId)
.map((param: any) => param.domainId); .map((param: any) => param.modelId);
const res = await Promise.all(domainIds.map((domainId: number) => getDimensionList(domainId))); const res = await Promise.all(modelIds.map((modelId: number) => getDimensionList(modelId)));
setDomainDimensionList( setModelDimensionList(
domainIds.reduce( modelIds.reduce((result: Record<number, DimensionType[]>, modelId: number, index: number) => {
(result: Record<number, DimensionType[]>, domainId: number, index: number) => { result[modelId] = res[index].data.list;
result[domainId] = res[index].data.list; return result;
return result; }, {}),
},
{},
),
); );
}; };
@@ -79,7 +74,7 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
(option: any) => option.paramType !== ParamTypeEnum.FORWARD, (option: any) => option.paramType !== ParamTypeEnum.FORWARD,
); );
setFilters(params); setFilters(params);
initDomainDimensions(params); initModelDimensions(params);
} }
setPluginType(detail.type); setPluginType(detail.type);
const parseModeObj = JSON.parse(detail.parseModeConfig || '{}'); const parseModeObj = JSON.parse(detail.parseModeConfig || '{}');
@@ -159,7 +154,7 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
await savePlugin({ await savePlugin({
...values, ...values,
id: detail?.id, id: detail?.id,
domainList: isArray(values.domainList) ? values.domainList : [values.domainList], modelList: isArray(values.modelList) ? values.modelList : [values.modelList],
config: JSON.stringify(config), config: JSON.stringify(config),
parseModeConfig: JSON.stringify(getFunctionParam(values.pattern)), parseModeConfig: JSON.stringify(getFunctionParam(values.pattern)),
}); });
@@ -169,11 +164,11 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
}; };
const updateDimensionList = async (value: number) => { const updateDimensionList = async (value: number) => {
if (domainDimensionList[value]) { if (modelDimensionList[value]) {
return; return;
} }
const res = await getDimensionList(value); const res = await getDimensionList(value);
setDomainDimensionList({ ...domainDimensionList, [value]: res.data.list }); setModelDimensionList({ ...modelDimensionList, [value]: res.data.list });
}; };
return ( return (
@@ -186,12 +181,12 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
onCancel={onCancel} onCancel={onCancel}
> >
<Form {...layout} form={form} style={{ maxWidth: 820 }}> <Form {...layout} form={form} style={{ maxWidth: 820 }}>
<FormItem name="domainList" label="主题域"> <FormItem name="modelList" label="主题域">
<Select <Select
placeholder="请选择主题域" placeholder="请选择主题域"
options={domainList.map((domain) => ({ options={modelList.map((model) => ({
label: domain.name, label: model.name,
value: domain.id, value: model.id,
}))} }))}
showSearch showSearch
filterOption={(input, option) => filterOption={(input, option) =>
@@ -223,7 +218,6 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
setPluginType(value); setPluginType(value);
if (value === PluginTypeEnum.DSL) { if (value === PluginTypeEnum.DSL) {
form.setFieldsValue({ parseMode: ParseModeEnum.FUNCTION_CALL }); form.setFieldsValue({ parseMode: ParseModeEnum.FUNCTION_CALL });
// setFunctionName('DSL');
setFunctionParams([ setFunctionParams([
{ {
id: uuid(), id: uuid(),
@@ -236,47 +230,6 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
}} }}
/> />
</FormItem> </FormItem>
<FormItem
name="pattern"
label="插件描述"
rules={[{ required: true, message: '请输入插件描述' }]}
>
<TextArea placeholder="请输入插件描述,多个描述换行分隔" allowClear />
</FormItem>
<FormItem name="exampleQuestions" label="示例问题">
<div className={styles.paramsSection}>
{examples.map((example) => {
const { id, question } = example;
return (
<div className={styles.filterRow} key={id}>
<Input
placeholder="示例问题"
value={question}
className={styles.questionExample}
onChange={(e) => {
example.question = e.target.value;
setExamples([...examples]);
}}
allowClear
/>
<DeleteOutlined
onClick={() => {
setExamples(examples.filter((item) => item.id !== id));
}}
/>
</div>
);
})}
<Button
onClick={() => {
setExamples([...examples, { id: uuid() }]);
}}
>
<PlusOutlined />
</Button>
</div>
</FormItem>
<FormItem label="函数名称"> <FormItem label="函数名称">
<Input <Input
value={functionName} value={functionName}
@@ -287,6 +240,9 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
allowClear allowClear
/> />
</FormItem> </FormItem>
<FormItem name="pattern" label="函数描述">
<TextArea placeholder="请输入函数描述,多个描述换行分隔" allowClear />
</FormItem>
<FormItem name="params" label="函数参数" hidden={pluginType === PluginTypeEnum.DSL}> <FormItem name="params" label="函数参数" hidden={pluginType === PluginTypeEnum.DSL}>
<div className={styles.paramsSection}> <div className={styles.paramsSection}>
{functionParams.map((functionParam: FunctionParamFormItemType) => { {functionParams.map((functionParam: FunctionParamFormItemType) => {
@@ -345,6 +301,40 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
</Button> </Button>
</div> </div>
</FormItem> </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>
{(pluginType === PluginTypeEnum.WEB_PAGE || pluginType === PluginTypeEnum.WEB_SERVICE) && ( {(pluginType === PluginTypeEnum.WEB_PAGE || pluginType === PluginTypeEnum.WEB_SERVICE) && (
<> <>
<FormItem name="url" label="地址" rules={[{ required: true, message: '请输入地址' }]}> <FormItem name="url" label="地址" rules={[{ required: true, message: '请输入地址' }]}>
@@ -391,9 +381,9 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
<> <>
<Select <Select
placeholder="主题域" placeholder="主题域"
options={domainList.map((domain) => ({ options={modelList.map((model) => ({
label: domain.name, label: model.name,
value: domain.id, value: model.id,
}))} }))}
showSearch showSearch
filterOption={(input, option) => filterOption={(input, option) =>
@@ -403,16 +393,16 @@ const DetailModal: React.FC<Props> = ({ detail, onSubmit, onCancel }) => {
} }
className={styles.filterParamName} className={styles.filterParamName}
allowClear allowClear
value={filter.domainId} value={filter.modelId}
onChange={(value) => { onChange={(value) => {
filter.domainId = value; filter.modelId = value;
setFilters([...filters]); setFilters([...filters]);
updateDimensionList(value); updateDimensionList(value);
}} }}
/> />
<Select <Select
placeholder="请选择维度,需先选择主题域" placeholder="请选择维度,需先选择主题域"
options={(domainDimensionList[filter.domainId] || []).map( options={(modelDimensionList[filter.modelId] || []).map(
(dimension) => ({ (dimension) => ({
label: dimension.name, label: dimension.name,
value: `${dimension.id}`, value: `${dimension.id}`,

View File

@@ -5,9 +5,9 @@ import moment from 'moment';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants'; import { PARSE_MODE_MAP, PLUGIN_TYPE_MAP } from './constants';
import DetailModal from './DetailModal'; import DetailModal from './DetailModal';
import { deletePlugin, getDomainList, getPluginList } from './service'; import { deletePlugin, getModelList, getPluginList } from './service';
import styles from './style.less'; import styles from './style.less';
import { DomainType, ParseModeEnum, PluginType, PluginTypeEnum } from './type'; import { ModelType, ParseModeEnum, PluginType, PluginTypeEnum } from './type';
const { Search } = Input; const { Search } = Input;
@@ -15,27 +15,27 @@ const PluginManage = () => {
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
const [type, setType] = useState<PluginTypeEnum>(); const [type, setType] = useState<PluginTypeEnum>();
const [pattern, setPattern] = useState<string>(); const [pattern, setPattern] = useState<string>();
const [domain, setDomain] = useState<string>(); const [model, setModel] = useState<string>();
const [data, setData] = useState<PluginType[]>([]); const [data, setData] = useState<PluginType[]>([]);
const [domainList, setDomainList] = useState<DomainType[]>([]); const [modelList, setModelList] = useState<ModelType[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginType>(); const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginType>();
const [detailModalVisible, setDetailModalVisible] = useState(false); const [detailModalVisible, setDetailModalVisible] = useState(false);
const initDomainList = async () => { const initModelList = async () => {
const res = await getDomainList(); const res = await getModelList();
setDomainList(getLeafList(res.data)); setModelList(getLeafList(res.data));
}; };
const updateData = async (filters?: any) => { const updateData = async (filters?: any) => {
setLoading(true); setLoading(true);
const res = await getPluginList({ name, type, pattern, domain, ...(filters || {}) }); const res = await getPluginList({ name, type, pattern, model, ...(filters || {}) });
setLoading(false); setLoading(false);
setData(res.data.map((item) => ({ ...item, config: JSON.parse(item.config || '{}') }))); setData(res.data?.map((item) => ({ ...item, config: JSON.parse(item.config || '{}') })) || []);
}; };
useEffect(() => { useEffect(() => {
initDomainList(); initModelList();
updateData(); updateData();
}, []); }, []);
@@ -58,17 +58,17 @@ const PluginManage = () => {
}, },
{ {
title: '主题域', title: '主题域',
dataIndex: 'domainList', dataIndex: 'modelList',
key: 'domainList', key: 'modelList',
width: 200, width: 200,
render: (value: number[]) => { render: (value: number[]) => {
if (value?.includes(-1)) { if (value?.includes(-1)) {
return '全部'; return '默认';
} }
return value ? ( return value ? (
<div className={styles.domainColumn}> <div className={styles.modelColumn}>
{value.map((id, index) => { {value.map((id, index) => {
const name = domainList.find((domain) => domain.id === +id)?.name; const name = modelList.find((model) => model.id === +id)?.name;
return name ? <Tag key={id}>{name}</Tag> : null; return name ? <Tag key={id}>{name}</Tag> : null;
})} })}
</div> </div>
@@ -90,7 +90,7 @@ const PluginManage = () => {
}, },
}, },
{ {
title: '插件描述', title: '函数描述',
dataIndex: 'pattern', dataIndex: 'pattern',
key: 'pattern', key: 'pattern',
width: 450, width: 450,
@@ -139,9 +139,9 @@ const PluginManage = () => {
}, },
]; ];
const onDomainChange = (value: string) => { const onModelChange = (value: string) => {
setDomain(value); setModel(value);
updateData({ domain: value }); updateData({ model: value });
}; };
const onTypeChange = (value: PluginTypeEnum) => { const onTypeChange = (value: PluginTypeEnum) => {
@@ -171,10 +171,10 @@ const PluginManage = () => {
<Select <Select
className={styles.filterItemControl} className={styles.filterItemControl}
placeholder="请选择主题域" placeholder="请选择主题域"
options={domainList.map((domain) => ({ label: domain.name, value: domain.id }))} options={modelList.map((model) => ({ label: model.name, value: model.id }))}
value={domain} value={model}
allowClear allowClear
onChange={onDomainChange} onChange={onModelChange}
/> />
</div> </div>
<div className={styles.filterItem}> <div className={styles.filterItem}>
@@ -190,10 +190,10 @@ const PluginManage = () => {
/> />
</div> </div>
<div className={styles.filterItem}> <div className={styles.filterItem}>
<div className={styles.filterItemTitle}></div> <div className={styles.filterItemTitle}></div>
<Search <Search
className={styles.filterItemControl} className={styles.filterItemControl}
placeholder="请输入插件描述" placeholder="请输入函数描述"
value={pattern} value={pattern}
onChange={(e) => { onChange={(e) => {
setPattern(e.target.value); setPattern(e.target.value);

View File

@@ -1,5 +1,5 @@
import { request } from "umi"; import { request } from "umi";
import { DimensionType, DomainType, PluginType } from "./type"; import { DimensionType, ModelType, PluginType } from "./type";
export function savePlugin(params: Partial<PluginType>) { export function savePlugin(params: Partial<PluginType>) {
return request<Result<any>>('/api/chat/plugin', { return request<Result<any>>('/api/chat/plugin', {
@@ -21,17 +21,17 @@ export function deletePlugin(id: number) {
}); });
} }
export function getDomainList() { export function getModelList() {
return request<Result<DomainType[]>>('/api/chat/conf/domainList', { return request<Result<ModelType[]>>('/api/chat/conf/modelList', {
method: 'GET', method: 'GET',
}); });
} }
export function getDimensionList(domainId: number) { export function getDimensionList(modelId: number) {
return request<Result<{list: DimensionType[]}>>('/api/semantic/dimension/queryDimension', { return request<Result<{list: DimensionType[]}>>('/api/semantic/dimension/queryDimension', {
method: 'POST', method: 'POST',
data: { data: {
domainIds: [domainId], modelIds: [modelId],
current: 1, current: 1,
pageSize: 2000 pageSize: 2000
} }

View File

@@ -47,7 +47,7 @@
} }
} }
.domainColumn { .modelColumn {
display: flex; display: flex;
align-items: center; align-items: center;
column-gap: 2px; column-gap: 2px;

View File

@@ -26,7 +26,7 @@ export enum ParamTypeEnum {
export type PluginType = { export type PluginType = {
id: number; id: number;
type: PluginTypeEnum; type: PluginTypeEnum;
domainList: number[]; modelList: number[];
pattern: string; pattern: string;
parseMode: ParseModeEnum; parseMode: ParseModeEnum;
parseModeConfig: string; parseModeConfig: string;
@@ -34,7 +34,7 @@ export type PluginType = {
config: PluginConfigType; config: PluginConfigType;
} }
export type DomainType = { export type ModelType = {
id: number | string; id: number | string;
parentId: number; parentId: number;
name: string; name: string;

View File

@@ -0,0 +1,160 @@
import IconFont from '@/components/IconFont';
import {
CaretRightOutlined,
CloseOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
} from '@ant-design/icons';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import Chat from '../Chat';
import { ModelType } from '../Chat/type';
import styles from './style.less';
import { useDispatch } from 'umi';
type Props = {
copilotSendMsg: string;
};
const Copilot: React.FC<Props> = ({ copilotSendMsg }) => {
const [chatVisible, setChatVisible] = useState(false);
const [defaultModelName, setDefaultModelName] = useState('');
const [fullscreen, setFullscreen] = useState(false);
const [triggerNewConversation, setTriggerNewConversation] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
const chatVisibleValue = localStorage.getItem('CHAT_VISIBLE') === 'true';
if (chatVisibleValue) {
setTimeout(() => {
setChatVisible(true);
}, 500);
}
}, []);
useEffect(() => {
if (copilotSendMsg) {
updateChatVisible(true);
setTriggerNewConversation(true);
}
}, [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}` : ''}`,
);
};
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>
</div>
<CaretRightOutlined className={styles.rightArrow} />
</div>
)}
</>
);
};
export default Copilot;

View File

@@ -0,0 +1,110 @@
.copilot {
position: fixed;
right: 8px;
bottom: 220px;
z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 54px;
height: 54px;
overflow: hidden;
color: #fff;
font-size: 26px;
background-color: var(--chat-blue);
background-clip: padding-box;
border: 2px solid #fff;
border-radius: 50%;
box-shadow: 8px 8px 20px 0 rgba(55, 99, 170, 0.1);
cursor: pointer;
transition: all 0.3s ease-in-out;
&:hover {
text-decoration: none;
box-shadow: 8px 8px 20px rgba(55, 99, 170, 0.3);
}
}
.chatPopover {
position: fixed;
right: 90px;
bottom: 5vh;
z-index: 999;
display: flex;
flex-direction: column;
width: 50vw;
height: 90vh;
overflow: hidden;
box-shadow: 8px 8px 20px rgba(55, 99, 170, 0.1), -2px -2px 16px rgba(55, 99, 170, 0.1);
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out,
-webkit-transform 0.3s ease-in-out;
.header {
position: relative;
z-index: 99;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
padding-right: 16px;
padding-left: 16px;
background: linear-gradient(90deg, #4692ff 0%, #1877ff 98%);
box-shadow: 1px 1px 8px #1b4aef5c;
.title {
color: #fff;
font-weight: 700;
font-size: 18px;
}
.leftSection {
position: absolute;
left: 16px;
display: flex;
align-items: center;
color: #fff;
font-size: 16px;
column-gap: 20px;
.close {
font-size: 18px;
cursor: pointer;
}
.transfer {
cursor: pointer;
}
.fullscreen {
font-size: 20px;
cursor: pointer;
}
}
}
.chat {
height: calc(90vh - 50px);
}
&.fullscreen {
bottom: 0;
left: 0;
width: calc(100vw - 90px);
height: 100vh;
.chat {
height: calc(100vh - 50px);
}
}
}
.rightArrow {
position: fixed;
right: 69px;
bottom: 232px;
z-index: 999;
color: var(--chat-blue);
font-size: 30px;
}

View File

@@ -16,7 +16,7 @@
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"suppressImplicitAnyIndexErrors": true, "ignoreDeprecations": "5.0",
"strict": true, "strict": true,
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],