(feature)(chat-sdk) add export error msg and export data (#1803)

This commit is contained in:
williamhliu
2024-10-15 10:24:27 +08:00
committed by GitHub
parent d7f301064a
commit 2ee77e39ff
7 changed files with 109 additions and 29 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 ? '' : ''}

View File

@@ -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>
)} )}

View File

@@ -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

View File

@@ -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} {

View File

@@ -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
}