mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-12 12:37:55 +00:00
(feature)(chat-sdk) add export error msg and export data (#1803)
This commit is contained in:
@@ -148,6 +148,7 @@ export type MsgDataType = {
|
|||||||
similarQueries: SimilarQuestionType[];
|
similarQueries: SimilarQuestionType[];
|
||||||
recommendedDimensions: DrillDownDimensionType[];
|
recommendedDimensions: DrillDownDimensionType[];
|
||||||
textResult: string;
|
textResult: string;
|
||||||
|
errorMsg: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ParseStateEnum {
|
export enum ParseStateEnum {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Spin, Row, Col, Switch } from 'antd';
|
import { Spin, Switch, Button } from 'antd';
|
||||||
import { CheckCircleFilled } from '@ant-design/icons';
|
import { CheckCircleFilled, DownloadOutlined } from '@ant-design/icons';
|
||||||
import { PREFIX_CLS, MsgContentTypeEnum } from '../../common/constants';
|
import { PREFIX_CLS, MsgContentTypeEnum } from '../../common/constants';
|
||||||
import { MsgDataType } from '../../common/type';
|
import { MsgDataType } from '../../common/type';
|
||||||
import ChatMsg from '../ChatMsg';
|
import ChatMsg from '../ChatMsg';
|
||||||
import WebPage from '../ChatMsg/WebPage';
|
import WebPage from '../ChatMsg/WebPage';
|
||||||
import Loading from './Loading';
|
import Loading from './Loading';
|
||||||
import React, { ReactNode, useState } from 'react';
|
import React, { ReactNode, useState } from 'react';
|
||||||
|
import { exportCsvFile } from '../../utils/utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryId?: number;
|
queryId?: number;
|
||||||
@@ -59,6 +60,20 @@ const ExecuteItem: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExportData = () => {
|
||||||
|
const { queryColumns, queryResults } = data || {};
|
||||||
|
if (!!queryResults) {
|
||||||
|
const exportData = queryResults.map(item => {
|
||||||
|
return Object.keys(item).reduce((result, key) => {
|
||||||
|
const columnName = queryColumns?.find(column => column.nameEn === key)?.name || key;
|
||||||
|
result[columnName] = item[key];
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
exportCsvFile(exportData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (executeLoading) {
|
if (executeLoading) {
|
||||||
return getNodeTip(`${titlePrefix}查询中`);
|
return getNodeTip(`${titlePrefix}查询中`);
|
||||||
}
|
}
|
||||||
@@ -67,7 +82,7 @@ const ExecuteItem: React.FC<Props> = ({
|
|||||||
return getNodeTip(
|
return getNodeTip(
|
||||||
<>
|
<>
|
||||||
{titlePrefix}查询失败
|
{titlePrefix}查询失败
|
||||||
{data?.queryTimeCost && isDeveloper && (
|
{!!data?.queryTimeCost && isDeveloper && (
|
||||||
<span className={`${prefixCls}-title-tip`}>(耗时: {data.queryTimeCost}ms)</span>
|
<span className={`${prefixCls}-title-tip`}>(耗时: {data.queryTimeCost}ms)</span>
|
||||||
)}
|
)}
|
||||||
</>,
|
</>,
|
||||||
@@ -83,28 +98,35 @@ const ExecuteItem: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<div className={`${prefixCls}-title-bar`}>
|
<div className={`${prefixCls}-title-bar`}>
|
||||||
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
||||||
<div className={`${prefixCls}-step-title`} style={{ width: '100%' }}>
|
<div
|
||||||
<Row style={{ width: '100%' }}>
|
className={`${prefixCls}-step-title ${prefixCls}-execute-title-bar`}
|
||||||
<Col flex="1 1 auto">
|
style={{ width: '100%' }}
|
||||||
{titlePrefix}查询
|
>
|
||||||
{data?.queryTimeCost && isDeveloper && (
|
<div>
|
||||||
<span className={`${prefixCls}-title-tip`}>(耗时: {data.queryTimeCost}ms)</span>
|
{titlePrefix}查询
|
||||||
)}
|
{!!data?.queryTimeCost && isDeveloper && (
|
||||||
</Col>
|
<span className={`${prefixCls}-title-tip`}>(耗时: {data.queryTimeCost}ms)</span>
|
||||||
<Col flex="0 1 70px">
|
)}
|
||||||
{[MsgContentTypeEnum.METRIC_TREND, MsgContentTypeEnum.METRIC_BAR].includes(
|
</div>
|
||||||
msgContentType as MsgContentTypeEnum
|
<div>
|
||||||
) && (
|
{[MsgContentTypeEnum.METRIC_TREND, MsgContentTypeEnum.METRIC_BAR].includes(
|
||||||
<Switch
|
msgContentType as MsgContentTypeEnum
|
||||||
checkedChildren="表格"
|
) && (
|
||||||
unCheckedChildren="表格"
|
<Switch
|
||||||
onChange={checked => {
|
checkedChildren="表格"
|
||||||
setShowMsgContentTable(checked);
|
unCheckedChildren="表格"
|
||||||
}}
|
onChange={checked => {
|
||||||
/>
|
setShowMsgContentTable(checked);
|
||||||
)}
|
}}
|
||||||
</Col>
|
/>
|
||||||
</Row>
|
)}
|
||||||
|
{!!data?.queryColumns?.length && (
|
||||||
|
<Button className={`${prefixCls}-export-data`} size="small" onClick={onExportData}>
|
||||||
|
<DownloadOutlined />
|
||||||
|
导出数据
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
return getNode(
|
return getNode(
|
||||||
<>
|
<>
|
||||||
意图解析失败
|
意图解析失败
|
||||||
{parseTimeCost && isDeveloper && (
|
{!!parseTimeCost && isDeveloper && (
|
||||||
<span className={`${prefixCls}-title-tip`}>(耗时: {parseTimeCost}ms)</span>
|
<span className={`${prefixCls}-title-tip`}>(耗时: {parseTimeCost}ms)</span>
|
||||||
)}
|
)}
|
||||||
</>,
|
</>,
|
||||||
@@ -338,7 +338,7 @@ const ParseTip: React.FC<Props> = ({
|
|||||||
<div className={`${prefixCls}-title-bar`}>
|
<div className={`${prefixCls}-title-bar`}>
|
||||||
<div>
|
<div>
|
||||||
意图解析
|
意图解析
|
||||||
{parseTimeCost && isDeveloper && (
|
{!!parseTimeCost && isDeveloper && (
|
||||||
<span className={`${prefixCls}-title-tip`}>(耗时: {parseTimeCost}ms)</span>
|
<span className={`${prefixCls}-title-tip`}>(耗时: {parseTimeCost}ms)</span>
|
||||||
)}
|
)}
|
||||||
{parseInfoOptions?.length > 1 ? ':' : ''}
|
{parseInfoOptions?.length > 1 ? ':' : ''}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Props = {
|
|||||||
queryMode?: string;
|
queryMode?: string;
|
||||||
sqlInfo: SqlInfoType;
|
sqlInfo: SqlInfoType;
|
||||||
sqlTimeCost?: number;
|
sqlTimeCost?: number;
|
||||||
|
executeErrorMsg: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SqlItem: React.FC<Props> = ({
|
const SqlItem: React.FC<Props> = ({
|
||||||
@@ -31,6 +32,7 @@ const SqlItem: React.FC<Props> = ({
|
|||||||
queryMode,
|
queryMode,
|
||||||
sqlInfo,
|
sqlInfo,
|
||||||
sqlTimeCost,
|
sqlTimeCost,
|
||||||
|
executeErrorMsg,
|
||||||
}) => {
|
}) => {
|
||||||
const [sqlType, setSqlType] = useState('');
|
const [sqlType, setSqlType] = useState('');
|
||||||
|
|
||||||
@@ -126,6 +128,14 @@ ${format(sqlInfo.querySQL)}
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getErrorMsgText = () => {
|
||||||
|
return `
|
||||||
|
异常日志
|
||||||
|
|
||||||
|
${executeErrorMsg}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
const onExportLog = () => {
|
const onExportLog = () => {
|
||||||
let text = '';
|
let text = '';
|
||||||
if (question) {
|
if (question) {
|
||||||
@@ -148,6 +158,9 @@ ${format(sqlInfo.querySQL)}
|
|||||||
if (sqlInfo.querySQL) {
|
if (sqlInfo.querySQL) {
|
||||||
text += getQuerySQLText();
|
text += getQuerySQLText();
|
||||||
}
|
}
|
||||||
|
if (!!executeErrorMsg) {
|
||||||
|
text += getErrorMsgText();
|
||||||
|
}
|
||||||
exportTextFile(text, `supersonic-debug-${agentId}-${queryId}.log`);
|
exportTextFile(text, `supersonic-debug-${agentId}-${queryId}.log`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,7 +170,7 @@ ${format(sqlInfo.querySQL)}
|
|||||||
<CheckCircleFilled className={`${tipPrefixCls}-step-icon`} />
|
<CheckCircleFilled className={`${tipPrefixCls}-step-icon`} />
|
||||||
<div className={`${tipPrefixCls}-step-title`}>
|
<div className={`${tipPrefixCls}-step-title`}>
|
||||||
SQL生成
|
SQL生成
|
||||||
{sqlTimeCost && (
|
{!!sqlTimeCost && (
|
||||||
<span className={`${tipPrefixCls}-title-tip`}>(耗时: {sqlTimeCost}ms)</span>
|
<span className={`${tipPrefixCls}-title-tip`}>(耗时: {sqlTimeCost}ms)</span>
|
||||||
)}
|
)}
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
const [executeMode, setExecuteMode] = useState(false);
|
const [executeMode, setExecuteMode] = useState(false);
|
||||||
const [executeLoading, setExecuteLoading] = useState(false);
|
const [executeLoading, setExecuteLoading] = useState(false);
|
||||||
const [executeTip, setExecuteTip] = useState('');
|
const [executeTip, setExecuteTip] = useState('');
|
||||||
|
const [executeErrorMsg, setExecuteErrorMsg] = useState('');
|
||||||
const [data, setData] = useState<MsgDataType>();
|
const [data, setData] = useState<MsgDataType>();
|
||||||
const [entitySwitchLoading, setEntitySwitchLoading] = useState(false);
|
const [entitySwitchLoading, setEntitySwitchLoading] = useState(false);
|
||||||
const [dimensionFilters, setDimensionFilters] = useState<FilterItemType[]>([]);
|
const [dimensionFilters, setDimensionFilters] = useState<FilterItemType[]>([]);
|
||||||
@@ -87,8 +88,9 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
const updateData = (res: Result<MsgDataType>) => {
|
const updateData = (res: Result<MsgDataType>) => {
|
||||||
let tip: string = '';
|
let tip: string = '';
|
||||||
let data: MsgDataType | undefined = undefined;
|
let data: MsgDataType | undefined = undefined;
|
||||||
const { queryColumns, queryResults, queryState, queryMode, response, chatContext } =
|
const { queryColumns, queryResults, queryState, queryMode, response, chatContext, errorMsg } =
|
||||||
res.data || {};
|
res.data || {};
|
||||||
|
setExecuteErrorMsg(errorMsg);
|
||||||
if (res.code === 400 || res.code === 401 || res.code === 412) {
|
if (res.code === 400 || res.code === 401 || res.code === 412) {
|
||||||
tip = res.msg;
|
tip = res.msg;
|
||||||
} else if (res.code !== 200) {
|
} else if (res.code !== 200) {
|
||||||
@@ -366,6 +368,7 @@ const ChatItem: React.FC<Props> = ({
|
|||||||
queryMode={parseInfo.queryMode}
|
queryMode={parseInfo.queryMode}
|
||||||
sqlInfo={parseInfo.sqlInfo}
|
sqlInfo={parseInfo.sqlInfo}
|
||||||
sqlTimeCost={parseTimeCost?.sqlTime}
|
sqlTimeCost={parseTimeCost?.sqlTime}
|
||||||
|
executeErrorMsg={executeErrorMsg}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ExecuteItem
|
<ExecuteItem
|
||||||
|
|||||||
@@ -151,6 +151,12 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-execute-title-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
&-reload {
|
&-reload {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@@ -392,6 +398,14 @@
|
|||||||
border-left: 1px solid var(--green);
|
border-left: 1px solid var(--green);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-export-data {
|
||||||
|
margin-left: 12px;
|
||||||
|
width: fit-content;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{filter-item-prefix-cls} {
|
.@{filter-item-prefix-cls} {
|
||||||
|
|||||||
@@ -303,3 +303,30 @@ export function exportTextFile(content: string, fileName: string, mimeType: stri
|
|||||||
// 释放 URL 对象
|
// 释放 URL 对象
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replacer(key: string, value: any) {
|
||||||
|
return value === null ? '' : value; // 将null值转换为空字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportCsvFile(data: any[]) {
|
||||||
|
// 生成CSV内容
|
||||||
|
const csvRows: any[] = [];
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
csvRows.push(headers.join(',')); // 添加表头
|
||||||
|
|
||||||
|
for (const row of data) {
|
||||||
|
csvRows.push(headers.map(header => JSON.stringify(row[header], replacer)).join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建Blob并下载文件
|
||||||
|
const csvString = '\ufeff' + csvRows.join('\n');
|
||||||
|
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'data.csv'; // 指定下载文件名
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(url); // 释放Blob URL
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user