mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-16 15:12:26 +00:00
Integrate Chat and Copilot into chat-sdk, and add SQL parse display (#166)
This commit is contained in:
@@ -58,7 +58,7 @@ const ExecuteItem: React.FC<Props> = ({
|
||||
<>
|
||||
<div className={`${prefixCls}-title-bar`}>
|
||||
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
||||
<div className={`${prefixCls}-step-title`}>数据查询结果</div>
|
||||
<div className={`${prefixCls}-step-title`}>数据查询</div>
|
||||
</div>
|
||||
<div className={`${prefixCls}-content-container ${prefixCls}-last-node`}>
|
||||
<Spin spinning={entitySwitchLoading}>
|
||||
@@ -68,7 +68,12 @@ const ExecuteItem: React.FC<Props> = ({
|
||||
{data?.queryMode === 'WEB_PAGE' ? (
|
||||
<WebPage id={queryId!} data={data} />
|
||||
) : (
|
||||
<ChatMsg data={data} chartIndex={chartIndex} triggerResize={triggerResize} />
|
||||
<ChatMsg
|
||||
queryId={queryId}
|
||||
data={data}
|
||||
chartIndex={chartIndex}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange
|
||||
const initData = async () => {
|
||||
const { data } = await queryDimensionValues(modelId, filter.bizName, '');
|
||||
setOptions(
|
||||
data?.data?.resultList.map((item: any) => ({
|
||||
data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
@@ -47,9 +47,8 @@ const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(
|
||||
newOptions.data?.data?.resultList.map((item: any) => ({
|
||||
newOptions.data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
@@ -76,7 +75,8 @@ const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange
|
||||
|
||||
return (
|
||||
<span className={prefixCls}>
|
||||
{typeof filter.value === 'string' || isArray(filter.value) ? (
|
||||
{(typeof filter.value === 'string' || isArray(filter.value)) &&
|
||||
(filter.operator === '=' || filter.operator === 'IN') ? (
|
||||
<Select
|
||||
bordered={false}
|
||||
value={filter.value}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||
import { ChatContextType, FilterItemType } from '../../common/type';
|
||||
import { CheckCircleFilled, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import SwicthEntity from './SwitchEntity';
|
||||
import Loading from './Loading';
|
||||
@@ -30,7 +30,7 @@ const ParseTip: React.FC<Props> = ({
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
const getNode = (tipTitle: string, tipNode?: ReactNode, parseSucceed?: boolean) => {
|
||||
const getNode = (tipTitle: ReactNode, tipNode?: ReactNode, parseSucceed?: boolean) => {
|
||||
const contentContainerClass = classNames(`${prefixCls}-content-container`, {
|
||||
[`${prefixCls}-content-container-succeed`]: parseSucceed,
|
||||
});
|
||||
@@ -62,7 +62,6 @@ const ParseTip: React.FC<Props> = ({
|
||||
|
||||
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||
const {
|
||||
modelId,
|
||||
modelName,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
@@ -76,8 +75,6 @@ const ParseTip: React.FC<Props> = ({
|
||||
nativeQuery,
|
||||
} = parseInfo || {};
|
||||
|
||||
const maxOptionCount = queryMode === 'DSL' ? 10 : MAX_OPTION_VALUES_COUNT;
|
||||
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
const dimensionItems = dimensions?.filter(item => item.type === 'DIMENSION');
|
||||
const metric = metrics?.[0];
|
||||
@@ -92,44 +89,6 @@ const ParseTip: React.FC<Props> = ({
|
||||
const fields =
|
||||
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
||||
|
||||
const getFilterContent = (filters: any) => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||
{filters.map((filter: any) => (
|
||||
<div className={`${prefixCls}-tip-item-option`}>
|
||||
<span>
|
||||
<span className={`${prefixCls}-tip-item-filter-name`}>{filter.name}</span>
|
||||
{filter.operator !== '=' && filter.operator !== 'IN'
|
||||
? ` ${filter.operator} `
|
||||
: ':'}
|
||||
</span>
|
||||
{queryMode !== 'DSL' && !filter.bizName?.includes('_id') ? (
|
||||
<FilterItem
|
||||
modelId={modelId}
|
||||
filters={dimensionFilters}
|
||||
filter={filter}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
) : (
|
||||
<span className={itemValueClass}>{filter.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFiltersNode = () => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-tip-item-content`}>
|
||||
{getFilterContent(dimensionFilters)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${prefixCls}-tip-content`}
|
||||
@@ -147,7 +106,7 @@ const ParseTip: React.FC<Props> = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(queryMode.includes('ENTITY') || queryMode === 'DSL') &&
|
||||
{(queryMode?.includes('ENTITY') || queryMode === 'DSL') &&
|
||||
typeof entityId === 'string' &&
|
||||
!!entityAlias &&
|
||||
!!entityName ? (
|
||||
@@ -165,11 +124,11 @@ const ParseTip: React.FC<Props> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>数据模型:</div>
|
||||
<div className={`${prefixCls}-tip-item-name`}>数据来源:</div>
|
||||
<div className={itemValueClass}>{modelName}</div>
|
||||
</div>
|
||||
)}
|
||||
{!queryMode.includes('ENTITY') && metric && (
|
||||
{!queryMode?.includes('ENTITY') && metric && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
||||
<div className={itemValueClass}>{metric.name}</div>
|
||||
@@ -199,24 +158,21 @@ const ParseTip: React.FC<Props> = ({
|
||||
</div>
|
||||
<div className={itemValueClass}>
|
||||
{fields
|
||||
.slice(0, maxOptionCount)
|
||||
.slice(0, MAX_OPTION_VALUES_COUNT)
|
||||
.map(field => field.name)
|
||||
.join('、')}
|
||||
{fields.length > maxOptionCount && '...'}
|
||||
{fields.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
'METRIC_FILTER',
|
||||
'METRIC_ENTITY',
|
||||
'ENTITY_DETAIL',
|
||||
'ENTITY_LIST_FILTER',
|
||||
'ENTITY_ID',
|
||||
'DSL',
|
||||
].includes(queryMode) &&
|
||||
dimensionFilters &&
|
||||
dimensionFilters?.length > 0 &&
|
||||
getFiltersNode()}
|
||||
{queryMode !== 'ENTITY_ID' &&
|
||||
entityDimensions?.length > 0 &&
|
||||
entityDimensions.map(dimension => (
|
||||
<div className={`${prefixCls}-tip-item`} key={dimension.itemId}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>{dimension.name}:</div>
|
||||
<div className={itemValueClass}>{dimension.value}</div>
|
||||
</div>
|
||||
))}
|
||||
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
||||
@@ -230,7 +186,8 @@ const ParseTip: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const parseInfo = parseInfoOptions[0] || {};
|
||||
const { properties, entity, entityInfo, elementMatches, queryMode } = parseInfo || {};
|
||||
const { modelId, properties, entity, entityInfo, elementMatches, queryMode, dimensionFilters } =
|
||||
parseInfo || {};
|
||||
|
||||
const { type } = properties || {};
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
@@ -249,31 +206,71 @@ const ParseTip: React.FC<Props> = ({
|
||||
)
|
||||
);
|
||||
|
||||
const getFilterContent = (filters: any) => {
|
||||
const itemValueClass = `${prefixCls}-tip-item-value`;
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||
{filters.map((filter: any) => (
|
||||
<div className={`${prefixCls}-tip-item-option`} key={filter.name}>
|
||||
<span>
|
||||
<span className={`${prefixCls}-tip-item-filter-name`}>{filter.name}</span>
|
||||
{filter.operator !== '=' && filter.operator !== 'IN' ? ` ${filter.operator} ` : ':'}
|
||||
</span>
|
||||
{/* {queryMode !== 'DSL' && !filter.bizName?.includes('_id') ? ( */}
|
||||
{!filter.bizName?.includes('_id') ? (
|
||||
<FilterItem
|
||||
modelId={modelId}
|
||||
filters={dimensionFilters}
|
||||
filter={filter}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
) : (
|
||||
<span className={itemValueClass}>{filter.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFiltersNode = () => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-tip-item-content`}>{getFilterContent(dimensionFilters)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
{getTipNode(parseInfo)}
|
||||
{queryMode !== 'ENTITY_ID' && entityDimensions?.length > 0 && (
|
||||
<div className={`${prefixCls}-entity-info`}>
|
||||
{entityDimensions.map(dimension => (
|
||||
<div className={`${prefixCls}-dimension-item`} key={dimension.itemId}>
|
||||
<div className={`${prefixCls}-dimension-name`}>{dimension.name}:</div>
|
||||
<div className={`${prefixCls}-dimension-value`}>{dimension.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
<InfoCircleOutlined />
|
||||
<div>
|
||||
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
'METRIC_FILTER',
|
||||
'METRIC_ENTITY',
|
||||
'ENTITY_DETAIL',
|
||||
'ENTITY_LIST_FILTER',
|
||||
'ENTITY_ID',
|
||||
'DSL',
|
||||
].includes(queryMode) &&
|
||||
dimensionFilters &&
|
||||
dimensionFilters?.length > 0 &&
|
||||
getFiltersNode()}
|
||||
</div>
|
||||
);
|
||||
|
||||
return getNode('意图解析结果', tipNode, true);
|
||||
return getNode(
|
||||
<div className={`${prefixCls}-title-bar`}>
|
||||
意图解析
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityAlias !== '厂牌' && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
(如果未匹配到您查询的{entityAlias},可点击{entityAlias}名切换)
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
tipNode,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
export default ParseTip;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CheckCircleFilled, DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { SimilarQuestionType } from '../../common/type';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
similarQuestions: SimilarQuestionType[];
|
||||
defaultExpanded?: boolean;
|
||||
onSelectQuestion: (question: SimilarQuestionType) => void;
|
||||
};
|
||||
|
||||
const SimilarQuestions: React.FC<Props> = ({
|
||||
similarQuestions,
|
||||
defaultExpanded,
|
||||
onSelectQuestion,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded || false);
|
||||
|
||||
const tipPrefixCls = `${PREFIX_CLS}-item`;
|
||||
const prefixCls = `${PREFIX_CLS}-similar-questions`;
|
||||
|
||||
const onToggleExpanded = () => {
|
||||
setExpanded(!expanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${tipPrefixCls}-parse-tip`}>
|
||||
<div className={`${tipPrefixCls}-title-bar`}>
|
||||
<CheckCircleFilled className={`${tipPrefixCls}-step-icon`} />
|
||||
<div className={`${tipPrefixCls}-step-title`}>
|
||||
推荐相似问题
|
||||
<span className={`${prefixCls}-toggle-expand-btn`} onClick={onToggleExpanded}>
|
||||
{expanded ? <UpOutlined /> : <DownOutlined />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={prefixCls}>
|
||||
{expanded && (
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{similarQuestions.slice(0, 5).map((question, index) => {
|
||||
return (
|
||||
<div
|
||||
className={`${prefixCls}-question`}
|
||||
key={question.queryText}
|
||||
onClick={() => {
|
||||
onSelectQuestion(question);
|
||||
}}
|
||||
>
|
||||
{index + 1}. {question.queryText}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimilarQuestions;
|
||||
116
webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx
Normal file
116
webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useState } from 'react';
|
||||
import { format } from 'sql-formatter';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { solarizedlight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { message } from 'antd';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { CheckCircleFilled, UpOutlined } from '@ant-design/icons';
|
||||
import { SqlInfoType } from '../../common/type';
|
||||
|
||||
type Props = {
|
||||
integrateSystem?: string;
|
||||
sqlInfo: SqlInfoType;
|
||||
};
|
||||
|
||||
const SqlItem: React.FC<Props> = ({ integrateSystem, sqlInfo }) => {
|
||||
const [sqlType, setSqlType] = useState('');
|
||||
|
||||
const tipPrefixCls = `${PREFIX_CLS}-item`;
|
||||
const prefixCls = `${PREFIX_CLS}-sql-item`;
|
||||
|
||||
const handleCopy = (text, result) => {
|
||||
result ? message.success('复制SQL成功', 1) : message.error('复制SQL失败', 1);
|
||||
};
|
||||
|
||||
const onCollapse = () => {
|
||||
setSqlType('');
|
||||
};
|
||||
|
||||
if (!sqlInfo.llmParseSql && !sqlInfo.logicSql && !sqlInfo.querySql) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${tipPrefixCls}-parse-tip`}>
|
||||
<div className={`${tipPrefixCls}-title-bar`}>
|
||||
<CheckCircleFilled className={`${tipPrefixCls}-step-icon`} />
|
||||
<div className={`${tipPrefixCls}-step-title`}>
|
||||
SQL生成
|
||||
{sqlType && (
|
||||
<span className={`${prefixCls}-toggle-expand-btn`} onClick={onCollapse}>
|
||||
<UpOutlined />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-sql-options`}>
|
||||
{sqlInfo.llmParseSql && (
|
||||
<div
|
||||
className={`${prefixCls}-sql-option ${
|
||||
sqlType === 'llmParseSql' ? `${prefixCls}-sql-option-active` : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSqlType(sqlType === 'llmParseSql' ? '' : 'llmParseSql');
|
||||
}}
|
||||
>
|
||||
LLM解析SQL
|
||||
</div>
|
||||
)}
|
||||
{sqlInfo.logicSql && (
|
||||
<div
|
||||
className={`${prefixCls}-sql-option ${
|
||||
sqlType === 'logicSql' ? `${prefixCls}-sql-option-active` : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSqlType(sqlType === 'logicSql' ? '' : 'logicSql');
|
||||
}}
|
||||
>
|
||||
逻辑SQL
|
||||
</div>
|
||||
)}
|
||||
{sqlInfo.querySql && (
|
||||
<div
|
||||
className={`${prefixCls}-sql-option ${
|
||||
sqlType === 'querySql' ? `${prefixCls}-sql-option-active` : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSqlType(sqlType === 'querySql' ? '' : 'querySql');
|
||||
}}
|
||||
>
|
||||
物理SQL
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${prefixCls} ${
|
||||
!window.location.pathname.includes('/chat') &&
|
||||
integrateSystem &&
|
||||
integrateSystem !== 'wiki'
|
||||
? `${prefixCls}-copilot`
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{sqlType && (
|
||||
<>
|
||||
<SyntaxHighlighter
|
||||
className={`${prefixCls}-code`}
|
||||
language="sql"
|
||||
style={solarizedlight}
|
||||
>
|
||||
{format(sqlInfo[sqlType])}
|
||||
</SyntaxHighlighter>
|
||||
<CopyToClipboard
|
||||
text={format(sqlInfo[sqlType])}
|
||||
onCopy={(text, result) => handleCopy(text, result)}
|
||||
>
|
||||
<button className={`${prefixCls}-copy-btn`}>复制代码</button>
|
||||
</CopyToClipboard>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SqlItem;
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ChatContextType, FilterItemType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import {
|
||||
ChatContextType,
|
||||
FilterItemType,
|
||||
MsgDataType,
|
||||
ParseStateEnum,
|
||||
SimilarQuestionType,
|
||||
} from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { chatExecute, chatParse, queryData, switchEntity } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||
@@ -8,6 +14,8 @@ import ExecuteItem from './ExecuteItem';
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import classNames from 'classnames';
|
||||
import Tools from '../Tools';
|
||||
import SqlItem from './SqlItem';
|
||||
import SimilarQuestionItem from './SimilarQuestionItem';
|
||||
|
||||
type Props = {
|
||||
msg: string;
|
||||
@@ -17,11 +25,13 @@ type Props = {
|
||||
filter?: any[];
|
||||
isLastMessage?: boolean;
|
||||
msgData?: MsgDataType;
|
||||
isHistory?: boolean;
|
||||
triggerResize?: boolean;
|
||||
parseOptions?: ChatContextType[];
|
||||
isDeveloper?: boolean;
|
||||
integrateSystem?: string;
|
||||
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
||||
onUpdateMessageScroll?: () => void;
|
||||
onSendMsg?: (msg: string) => void;
|
||||
};
|
||||
|
||||
const ChatItem: React.FC<Props> = ({
|
||||
@@ -31,12 +41,14 @@ const ChatItem: React.FC<Props> = ({
|
||||
agentId,
|
||||
filter,
|
||||
isLastMessage,
|
||||
isHistory,
|
||||
triggerResize,
|
||||
msgData,
|
||||
parseOptions,
|
||||
isDeveloper,
|
||||
integrateSystem,
|
||||
onMsgDataLoaded,
|
||||
onUpdateMessageScroll,
|
||||
onSendMsg,
|
||||
}) => {
|
||||
const [data, setData] = useState<MsgDataType>();
|
||||
const [parseLoading, setParseLoading] = useState(false);
|
||||
@@ -47,6 +59,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
const [executeTip, setExecuteTip] = useState('');
|
||||
const [executeMode, setExecuteMode] = useState(false);
|
||||
const [entitySwitchLoading, setEntitySwitchLoading] = useState(false);
|
||||
const [similarQuestions, setSimilarQuestions] = useState<SimilarQuestionType[]>([]);
|
||||
|
||||
const [chartIndex, setChartIndex] = useState(0);
|
||||
|
||||
@@ -79,13 +92,13 @@ const ChatItem: React.FC<Props> = ({
|
||||
setExecuteMode(true);
|
||||
setExecuteLoading(true);
|
||||
try {
|
||||
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||
const res: any = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||
setExecuteLoading(false);
|
||||
const valid = updateData(data);
|
||||
const valid = updateData(res);
|
||||
if (onMsgDataLoaded) {
|
||||
onMsgDataLoaded(
|
||||
{
|
||||
...data.data,
|
||||
...res.data,
|
||||
chatContext: parseInfoValue,
|
||||
},
|
||||
valid
|
||||
@@ -97,12 +110,13 @@ const ChatItem: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onSendMsg = async () => {
|
||||
const sendMsg = async () => {
|
||||
setParseLoading(true);
|
||||
const { data: parseData } = await chatParse(msg, conversationId, modelId, agentId, filter);
|
||||
const parseData: any = await chatParse(msg, conversationId, modelId, agentId, filter);
|
||||
setParseLoading(false);
|
||||
const { code, data } = parseData || {};
|
||||
const { state, selectedParses, queryId } = data || {};
|
||||
const { state, selectedParses, queryId, similarSolvedQuery } = data || {};
|
||||
setSimilarQuestions(similarSolvedQuery || []);
|
||||
if (
|
||||
code !== 200 ||
|
||||
state === ParseStateEnum.FAILED ||
|
||||
@@ -115,7 +129,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
const parseInfos = selectedParses.map(item => ({
|
||||
const parseInfos = selectedParses.map((item: any) => ({
|
||||
...item,
|
||||
queryId,
|
||||
}));
|
||||
@@ -126,15 +140,17 @@ const ChatItem: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data !== undefined || parseOptions !== undefined || executeTip !== '') {
|
||||
if (data !== undefined || parseOptions !== undefined || executeTip !== '' || parseLoading) {
|
||||
return;
|
||||
}
|
||||
if (msgData) {
|
||||
setParseInfoOptions([msgData.chatContext]);
|
||||
const parseInfoValue = { ...msgData.chatContext, queryId: msgData.queryId };
|
||||
setParseInfoOptions([parseInfoValue]);
|
||||
setParseInfo(parseInfoValue);
|
||||
setExecuteMode(true);
|
||||
updateData({ code: 200, data: msgData, msg: 'success' });
|
||||
} else if (msg) {
|
||||
onSendMsg();
|
||||
sendMsg();
|
||||
}
|
||||
}, [msg, msgData]);
|
||||
|
||||
@@ -142,19 +158,27 @@ const ChatItem: React.FC<Props> = ({
|
||||
setEntitySwitchLoading(true);
|
||||
const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
|
||||
setEntitySwitchLoading(false);
|
||||
setData(res.data.data);
|
||||
const { chatContext } = res.data.data;
|
||||
setData(res.data);
|
||||
const { chatContext } = res.data;
|
||||
setParseInfo(chatContext);
|
||||
setParseInfoOptions([chatContext]);
|
||||
};
|
||||
|
||||
const onFiltersChange = async (dimensionFilters: FilterItemType[]) => {
|
||||
setEntitySwitchLoading(true);
|
||||
const chatContextValue = { ...(parseInfoOptions[0] || {}), dimensionFilters };
|
||||
const { dimensions, metrics, dateInfo, id, queryId } = parseInfoOptions[0] || {};
|
||||
const chatContextValue = {
|
||||
dimensions,
|
||||
metrics,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
parseId: id,
|
||||
queryId,
|
||||
};
|
||||
const res: any = await queryData(chatContextValue);
|
||||
setEntitySwitchLoading(false);
|
||||
const resChatContext = res.data?.data?.chatContext;
|
||||
setData({ ...(res.data?.data || {}), chatContext: resChatContext || chatContextValue });
|
||||
const resChatContext = res.data?.chatContext;
|
||||
setData({ ...(res.data || {}), chatContext: resChatContext || chatContextValue });
|
||||
setParseInfo(resChatContext || chatContextValue);
|
||||
setParseInfoOptions([resChatContext || chatContextValue]);
|
||||
};
|
||||
@@ -167,6 +191,10 @@ const ChatItem: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectQuestion = (question: SimilarQuestionType) => {
|
||||
onSendMsg?.(question.queryText);
|
||||
};
|
||||
|
||||
const contentClass = classNames(`${prefixCls}-content`, {
|
||||
[`${prefixCls}-content-mobile`]: isMobile,
|
||||
});
|
||||
@@ -189,17 +217,36 @@ const ChatItem: React.FC<Props> = ({
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
{executeMode && (
|
||||
<ExecuteItem
|
||||
queryId={parseInfo?.queryId}
|
||||
executeLoading={executeLoading}
|
||||
entitySwitchLoading={entitySwitchLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
triggerResize={triggerResize}
|
||||
{parseTip && similarQuestions.length > 0 && (
|
||||
<SimilarQuestionItem
|
||||
similarQuestions={similarQuestions}
|
||||
defaultExpanded
|
||||
onSelectQuestion={onSelectQuestion}
|
||||
/>
|
||||
)}
|
||||
{executeMode && (
|
||||
<>
|
||||
{parseInfoOptions?.[0]?.sqlInfo && isDeveloper && integrateSystem !== 'c2' && (
|
||||
<SqlItem integrateSystem={integrateSystem} sqlInfo={parseInfoOptions[0].sqlInfo} />
|
||||
)}
|
||||
{similarQuestions.length > 0 && (
|
||||
<SimilarQuestionItem
|
||||
similarQuestions={similarQuestions}
|
||||
defaultExpanded={executeTip !== ''}
|
||||
onSelectQuestion={onSelectQuestion}
|
||||
/>
|
||||
)}
|
||||
<ExecuteItem
|
||||
queryId={parseInfo?.queryId}
|
||||
executeLoading={executeLoading}
|
||||
entitySwitchLoading={entitySwitchLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isMetricCard && data && (
|
||||
<Tools data={data} scoreValue={undefined} isLastMessage={isLastMessage} />
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
@chat-item-prefix-cls: ~'@{supersonic-chat-prefix}-item';
|
||||
@filter-item-prefix-cls: ~'@{supersonic-chat-prefix}-filter-item';
|
||||
@sql-item-prefix-cls: ~'@{supersonic-chat-prefix}-sql-item';
|
||||
@similar-questions-prefix-cls: ~'@{supersonic-chat-prefix}-similar-questions';
|
||||
|
||||
.@{chat-item-prefix-cls} {
|
||||
display: flex;
|
||||
@@ -17,7 +19,6 @@
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
// border-radius: 50%;
|
||||
background-color: var(--text-color);
|
||||
margin: 0 2px;
|
||||
opacity: 0;
|
||||
@@ -124,8 +125,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third);
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
&-tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 6px;
|
||||
row-gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
row-gap: 10px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
@@ -251,7 +251,6 @@
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
column-gap: 12px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -325,4 +324,93 @@
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{sql-item-prefix-cls} {
|
||||
position: relative;
|
||||
margin: 2px 0 2px 7px;
|
||||
padding: 2px 0 8px 18px;
|
||||
border-left: 1px solid var(--green);
|
||||
overflow: auto;
|
||||
|
||||
&-toggle-expand-btn {
|
||||
margin-left: 4px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-sql-options {
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 13px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-sql-option {
|
||||
border-radius: 4px;
|
||||
padding: 1px 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-sql-option-active {
|
||||
color: #fff !important;
|
||||
background-color: var(--chat-blue);
|
||||
}
|
||||
|
||||
&-code {
|
||||
margin-top: 10px !important;
|
||||
padding: 6px 14px 8px !important;
|
||||
border: 1px solid var(--border-color-base) !important;
|
||||
border-radius: 4px !important;
|
||||
background: #f5f8fb !important;
|
||||
}
|
||||
|
||||
&-copy-btn {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 20px;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
color: var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.@{sql-item-prefix-cls}-copilot {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.@{similar-questions-prefix-cls} {
|
||||
position: relative;
|
||||
margin: 2px 0 2px 7px;
|
||||
padding: 2px 0 8px 18px;
|
||||
border-left: 1px solid var(--green);
|
||||
overflow: auto;
|
||||
|
||||
&-toggle-expand-btn {
|
||||
margin-left: 4px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&-question {
|
||||
width: fit-content;
|
||||
color: var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ const Message: React.FC<Props> = ({
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
<div className={`${prefixCls}-main-entity-info`}>
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
<div className={`${prefixCls}-info-name`}>数据模型:</div>
|
||||
<div className={`${prefixCls}-info-name`}>数据来源:</div>
|
||||
<div className={`${prefixCls}-info-value`}>{modelName}</div>
|
||||
</div>
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
|
||||
@@ -19,33 +19,7 @@ const Text: React.FC<Props> = ({ columns, referenceColumn, dataSource }) => {
|
||||
|
||||
const initData = () => {
|
||||
let textValue = dataSource[0][columns[0].nameEn];
|
||||
let htmlCodeValue: string;
|
||||
const match = textValue.match(/```html([\s\S]*?)```/);
|
||||
htmlCodeValue = match && match[1].trim();
|
||||
if (htmlCodeValue) {
|
||||
textValue = textValue.replace(/```html([\s\S]*?)```/, '');
|
||||
}
|
||||
let scriptCode: string;
|
||||
let scriptSrc: string;
|
||||
if (htmlCodeValue) {
|
||||
scriptSrc = htmlCodeValue.match(/<script src="([\s\S]*?)"><\/script>/)?.[1] || '';
|
||||
scriptCode =
|
||||
htmlCodeValue.match(/<script type="text\/javascript">([\s\S]*?)<\/script>/)?.[1] || '';
|
||||
if (scriptSrc) {
|
||||
const script = document.createElement('script');
|
||||
script.src = scriptSrc;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
if (scriptCode) {
|
||||
const script = document.createElement('script');
|
||||
script.innerHTML = scriptCode;
|
||||
setTimeout(() => {
|
||||
document.body.appendChild(script);
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
setText(textValue);
|
||||
setHtmlCode(htmlCodeValue);
|
||||
setText(textValue === undefined ? '暂无数据,如有疑问请联系管理员' : textValue);
|
||||
if (referenceColumn) {
|
||||
const referenceDataValue = dataSource[0][referenceColumn.nameEn];
|
||||
setReferenceData(referenceDataValue || []);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CLS_PREFIX } from '../../../common/constants';
|
||||
import { MsgDataType } from '../../../common/type';
|
||||
import { isProd } from '../../../utils/utils';
|
||||
import { getToken, isProd } from '../../../utils/utils';
|
||||
|
||||
type Props = {
|
||||
id: string | number;
|
||||
@@ -89,10 +89,8 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
);
|
||||
urlValue = urlValue.replace(
|
||||
'?',
|
||||
`?miniProgram=true&reportName=${name}&filterData=${filterData}&`
|
||||
`?token=${getToken()}&miniProgram=true&reportName=${name}&filterData=${filterData}&`
|
||||
);
|
||||
urlValue =
|
||||
!isProd() && !urlValue.includes('http') ? `http://s2.tmeoa.com${urlValue}` : urlValue;
|
||||
} else {
|
||||
const params = Object.keys(valueParams || {}).map(key => `${key}=${valueParams[key]}`);
|
||||
if (params.length > 0) {
|
||||
@@ -103,7 +101,6 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// onReportLoaded(heightValue + 190);
|
||||
setPluginUrl(urlValue);
|
||||
};
|
||||
|
||||
@@ -112,7 +109,6 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// <div className={prefixCls} style={{ height }}>
|
||||
<iframe
|
||||
id={`reportIframe_${id}`}
|
||||
name={`reportIframe_${id}`}
|
||||
@@ -121,7 +117,6 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
title="reportIframe"
|
||||
allowFullScreen
|
||||
/>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import DrillDownDimensions from '../DrillDownDimensions';
|
||||
import MetricOptions from '../MetricOptions';
|
||||
|
||||
type Props = {
|
||||
queryId?: number;
|
||||
data: MsgDataType;
|
||||
chartIndex: number;
|
||||
triggerResize?: boolean;
|
||||
};
|
||||
|
||||
const ChatMsg: React.FC<Props> = ({ data, chartIndex, triggerResize }) => {
|
||||
const ChatMsg: React.FC<Props> = ({ queryId, data, chartIndex, triggerResize }) => {
|
||||
const { queryColumns, queryResults, chatContext, queryMode } = data || {};
|
||||
const { dimensionFilters, elementMatches } = chatContext || {};
|
||||
|
||||
@@ -127,14 +128,16 @@ const ChatMsg: React.FC<Props> = ({ data, chartIndex, triggerResize }) => {
|
||||
|
||||
const onLoadData = async (value: any) => {
|
||||
setLoading(true);
|
||||
const { data } = await queryData({
|
||||
...chatContext,
|
||||
const res: any = await queryData({
|
||||
// ...chatContext,
|
||||
queryId,
|
||||
parseId: chatContext.id,
|
||||
...value,
|
||||
});
|
||||
setLoading(false);
|
||||
if (data.code === 200) {
|
||||
updateColummns(data.data?.queryColumns || []);
|
||||
setDataSource(data.data?.queryResults || []);
|
||||
if (res.code === 200) {
|
||||
updateColummns(res.data?.queryColumns || []);
|
||||
setDataSource(res.data?.queryResults || []);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const DrillDownDimensions: React.FC<Props> = ({
|
||||
const initData = async () => {
|
||||
const res = await queryDrillDownDimensions(modelId);
|
||||
setDimensions(
|
||||
res.data.data.dimensions
|
||||
res.data.dimensions
|
||||
.filter(
|
||||
dimension =>
|
||||
!dimensionFilters?.some(filter => filter.name === dimension.name) &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_x5c4www9bqm.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_46xw04fpzii.js',
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -25,7 +25,7 @@ const RecommendOptions: React.FC<Props> = ({ entityId, modelId, modelName, onSel
|
||||
setLoading(true);
|
||||
const res = await queryEntities(entityId, modelId);
|
||||
setLoading(false);
|
||||
setData(res.data.data);
|
||||
setData(res.data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user