From e571ca1f55a2281dba0ad32c96a2e029ab42d2d8 Mon Sep 17 00:00:00 2001 From: williamhliu <137068196+williamhliu@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:20:02 +0800 Subject: [PATCH] (feature)(chat-sdk) add export log (#1482) --- .../src/components/ChatItem/ParseTip.tsx | 15 +-- .../src/components/ChatItem/SqlItem.tsx | 102 +++++++++++++++++- .../src/components/ChatItem/index.tsx | 9 +- .../src/components/ChatItem/style.less | 8 ++ webapp/packages/chat-sdk/src/service/index.ts | 6 -- webapp/packages/chat-sdk/src/utils/utils.ts | 62 ++++++++--- 6 files changed, 157 insertions(+), 45 deletions(-) diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx b/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx index 1d5df8614..8ce72dfb8 100644 --- a/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx @@ -143,19 +143,6 @@ const ParseTip: React.FC = ({ const entityAlias = entity?.alias?.[0]?.split('.')?.[0]; - const entityDimensions = entityInfo?.dimensions?.filter( - item => - !['zyqk_song_id', 'song_name', 'singer_id', 'zyqk_cmpny_id'].includes(item.bizName) && - !( - entityInfo?.dimensions?.some(dimension => dimension.bizName === 'singer_id') && - item.bizName === 'singer_name' - ) && - !( - entityInfo?.dimensions?.some(dimension => dimension.bizName === 'zyqk_cmpny_id') && - item.bizName === 'cmpny_name' - ) - ); - const getTipNode = () => { const dimensionItems = dimensions?.filter(item => item.type === 'DIMENSION'); const itemValueClass = `${prefixCls}-tip-item-value`; @@ -230,7 +217,7 @@ const ParseTip: React.FC = ({ )} {queryMode !== 'TAG_ID' && !dimensions?.some(item => item.bizName?.includes('_id')) && - entityDimensions + entityInfo?.dimensions ?.filter(dimension => dimension.value != null) .map(dimension => (
diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx b/webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx index 499c01989..492176474 100644 --- a/webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx @@ -3,10 +3,11 @@ 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 { Button, message } from 'antd'; import { PREFIX_CLS } from '../../common/constants'; -import { CheckCircleFilled, UpOutlined } from '@ant-design/icons'; +import { CheckCircleFilled, DownloadOutlined, UpOutlined } from '@ant-design/icons'; import { SqlInfoType } from '../../common/type'; +import { exportTextFile } from '../../utils/utils'; type Props = { llmReq?: any; @@ -46,6 +47,99 @@ const SqlItem: React.FC = ({ const fewShots = (Object.values(llmResp?.sqlRespMap || {})[0] as any)?.fewShots || []; + const getSchemaMapText = () => { + return ` +Schema映射 +${schema?.fieldNameList?.length > 0 ? `名称:${schema.fieldNameList.join('、')}` : ''}${ + linking?.length > 0 + ? ` +取值:${linking + .map((item: any) => { + return `${item.fieldName}: ${item.fieldValue}`; + }) + .join('、')}` + : '' + }${ + priorExts + ? ` +附加:${priorExts}` + : '' + }${ + schema?.terms?.length > 0 + ? ` +术语:${schema.terms + .map((item: any) => { + return `${item.name}${item.alias?.length > 0 ? `(${item.alias.join(',')})` : ''}: ${ + item.description + }`; + }) + .join('、')}` + : '' + } + +`; + }; + + const getFewShotText = () => { + return ` +Few-shot示例${fewShots + .map((item: any, index: number) => { + return ` + +示例${index + 1}: +问题:${item.question} +SQL: +${format(item.sql)} +`; + }) + .join('')} +`; + }; + + const getParsedS2SQLText = () => { + return ` +${queryMode === 'LLM_S2SQL' || queryMode === 'PLAIN_TEXT' ? 'LLM' : 'Rule'}解析S2SQL + +${format(sqlInfo.parsedS2SQL)} +`; + }; + + const getCorrectedS2SQLText = () => { + return ` +修正S2SQL + +${format(sqlInfo.correctedS2SQL)} +`; + }; + + const getQuerySQLText = () => { + return ` +最终执行SQL + +${format(sqlInfo.querySQL)} +`; + }; + + const onExportLog = () => { + let text = ''; + if (llmReq) { + text += getSchemaMapText(); + } + if (fewShots.length > 0) { + text += getFewShotText(); + } + if (sqlInfo.parsedS2SQL) { + text += getParsedS2SQLText(); + } + if (sqlInfo.correctedS2SQL) { + text += getCorrectedS2SQLText(); + } + if (sqlInfo.querySQL) { + text += getQuerySQLText(); + } + exportTextFile(text, 'file.txt'); + }; + return (
@@ -123,6 +217,10 @@ const SqlItem: React.FC = ({ 最终执行SQL
)} +
= ({ } }; - const getEntityInfo = async (parseInfoValue: ChatContextType) => { - const res = await queryEntityInfo(parseInfoValue.queryId, parseInfoValue.id); - setEntityInfo(res.data); - }; - const onSelectParseInfo = async (parseInfoValue: ChatContextType) => { setParseInfo(parseInfoValue); updateDimensionFitlers(parseInfoValue.dimensionFilters || []); setDateInfo(parseInfoValue.dateInfo); if (parseInfoValue.entityInfo) { setEntityInfo(parseInfoValue.entityInfo); - } else { - getEntityInfo(parseInfoValue); } if (dataCache[parseInfoValue.id!]) { const { tip, data } = dataCache[parseInfoValue.id!]; diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/style.less b/webapp/packages/chat-sdk/src/components/ChatItem/style.less index c4cdef6a5..5cfaf4a07 100644 --- a/webapp/packages/chat-sdk/src/components/ChatItem/style.less +++ b/webapp/packages/chat-sdk/src/components/ChatItem/style.less @@ -541,6 +541,14 @@ padding: 0 !important; background-color: transparent !important; } + + &-export-log { + margin-left: 20px; + width: fit-content; + font-weight: normal; + color: var(--text-color-secondary); + font-size: 13px !important; + } } .@{sql-item-prefix-cls}-copilot { diff --git a/webapp/packages/chat-sdk/src/service/index.ts b/webapp/packages/chat-sdk/src/service/index.ts index a00f84abd..6e751fc2a 100644 --- a/webapp/packages/chat-sdk/src/service/index.ts +++ b/webapp/packages/chat-sdk/src/service/index.ts @@ -131,9 +131,3 @@ export function queryDimensionValues( value, }); } - -export function queryEntityInfo(queryId: number, parseId: number) { - return axios.get( - `${prefix}/chat/query/getEntityInfo?queryId=${queryId}&parseId=${parseId}` - ); -} diff --git a/webapp/packages/chat-sdk/src/utils/utils.ts b/webapp/packages/chat-sdk/src/utils/utils.ts index 8e51b48f3..ace00cef4 100644 --- a/webapp/packages/chat-sdk/src/utils/utils.ts +++ b/webapp/packages/chat-sdk/src/utils/utils.ts @@ -70,13 +70,13 @@ export const getFormattedValue = (value: number | string, remainZero?: boolean) +value >= 100000000 ? NumericUnit.OneHundredMillion : +value >= 10000 - ? NumericUnit.TenThousand - : NumericUnit.None; + ? NumericUnit.TenThousand + : NumericUnit.None; let formattedValue = formatByUnit(value, unit); formattedValue = formatByDecimalPlaces( formattedValue, - unit === NumericUnit.OneHundredMillion ? 2 : +value < 1 ? 3 : 1, + unit === NumericUnit.OneHundredMillion ? 2 : +value < 1 ? 3 : 1 ); formattedValue = formatByThousandSeperator(formattedValue); if ((typeof formattedValue === 'number' && isNaN(formattedValue)) || +formattedValue === 0) { @@ -88,11 +88,11 @@ export const getFormattedValue = (value: number | string, remainZero?: boolean) export const formatNumberWithCN = (num: number) => { if (isNaN(num)) return '-'; if (num >= 10000) { - return (num / 10000).toFixed(1) + "万"; + return (num / 10000).toFixed(1) + '万'; } else { return formatByDecimalPlaces(num, 2); } -} +}; export const groupByColumn = (data: any[], column: string) => { return data.reduce((result, item) => { @@ -124,11 +124,15 @@ export const normalizeTrendData = ( valueColumnName: string, startDate: string, endDate: string, - dateType?: string, + dateType?: string ) => { const dateList = enumerateDaysBetweenDates(moment(startDate), moment(endDate), dateType); - const result = dateList.map((date) => { - const item = resultList.find((result) => moment(result[dateColumnName]).format(dateType === 'months' ? 'YYYY-MM' : 'YYYY-MM-DD') === date); + const result = dateList.map(date => { + const item = resultList.find( + result => + moment(result[dateColumnName]).format(dateType === 'months' ? 'YYYY-MM' : 'YYYY-MM-DD') === + date + ); return { ...(item || {}), [dateColumnName]: date, @@ -139,7 +143,7 @@ export const normalizeTrendData = ( }; export const getMinMaxDate = (resultList: any[], dateColumnName: string) => { - const dateList = resultList.map((item) => moment(item[dateColumnName])); + const dateList = resultList.map(item => moment(item[dateColumnName])); return [moment.min(dateList).format('YYYY-MM-DD'), moment.max(dateList).format('YYYY-MM-DD')]; }; @@ -147,10 +151,10 @@ export function hexToRgbObj(hex) { var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - } + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } : null; } @@ -177,7 +181,6 @@ export const isIOS = window.navigator.userAgent.match(/(iPhone|iPod|ios)/i); export const isAndroid = window.navigator.userAgent.match(/(Android)/i); - export function isProd() { return process.env.NODE_ENV === 'production'; } @@ -248,7 +251,7 @@ export const getTextWidth = ( text: string, fontSize: string = '16px', fontWeight: string = 'normal', - fontFamily: string = 'DINPro Medium', + fontFamily: string = 'DINPro Medium' ): number => { const canvas = utilCanvas || (utilCanvas = document.createElement('canvas')); const context = canvas.getContext('2d'); @@ -271,3 +274,32 @@ export function jsonParse(config: any, defaultReturn?: any) { return defaultReturn; } } + +/** + * 导出文本文件的函数 + * @param content - 要导出的文本内容 + * @param fileName - 导出的文件名 + * @param mimeType - 文件的 MIME 类型,默认为 'text/plain' + */ +export function exportTextFile(content: string, fileName: string, mimeType: string = 'text/plain') { + // 创建一个 Blob 对象 + const blob = new Blob([content], { type: mimeType }); + + // 创建一个 URL 对象 + const url = URL.createObjectURL(blob); + + // 创建一个 元素 + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + + // 触发下载 + document.body.appendChild(a); + a.click(); + + // 移除 元素 + document.body.removeChild(a); + + // 释放 URL 对象 + URL.revokeObjectURL(url); +}