diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/enums/TextType.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/enums/TextType.java index fefa392da..58a8b3b81 100644 --- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/enums/TextType.java +++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/enums/TextType.java @@ -33,6 +33,8 @@ public enum TextType { MARK_DOWN("```", "```"), + RESULT_SET("```result_set", "```"), + TEXT(null, null); private final String startSign; diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java index 840a7893b..774125f19 100644 --- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java +++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java @@ -210,13 +210,17 @@ private Map executeSqlQuery(OverAllState state, Integer currentS // Create display flux for user experience only Flux displayFlux = Flux.create(emitter -> { + // todo: 先返回Flux流,再去执行SQL查询 emitter.next(ChatResponseUtil.createResponse("开始执行SQL...")); emitter.next(ChatResponseUtil.createResponse("执行SQL查询")); emitter.next(ChatResponseUtil.createPureResponse(TextType.SQL.getStartSign())); emitter.next(ChatResponseUtil.createResponse(sqlQuery)); emitter.next(ChatResponseUtil.createPureResponse(TextType.SQL.getEndSign())); emitter.next(ChatResponseUtil.createResponse("执行SQL完成")); - // todo: 输出SQL执行结果 + emitter.next(ChatResponseUtil.createResponse("SQL查询结果:")); + emitter.next(ChatResponseUtil.createPureResponse(TextType.RESULT_SET.getStartSign())); + emitter.next(ChatResponseUtil.createPureResponse(jsonStr)); + emitter.next(ChatResponseUtil.createPureResponse(TextType.RESULT_SET.getEndSign())); emitter.complete(); }); diff --git a/spring-ai-alibaba-data-agent-frontend/src/services/graph.ts b/spring-ai-alibaba-data-agent-frontend/src/services/graph.ts index 79b13ed9a..19bf353d2 100644 --- a/spring-ai-alibaba-data-agent-frontend/src/services/graph.ts +++ b/spring-ai-alibaba-data-agent-frontend/src/services/graph.ts @@ -41,6 +41,7 @@ export enum TextType { SQL = 'SQL', HTML = 'HTML', MARK_DOWN = 'MARK_DOWN', + RESULT_SET = 'RESULT_SET', TEXT = 'TEXT', } diff --git a/spring-ai-alibaba-data-agent-frontend/src/services/resultSet.ts b/spring-ai-alibaba-data-agent-frontend/src/services/resultSet.ts new file mode 100644 index 000000000..4d74b6197 --- /dev/null +++ b/spring-ai-alibaba-data-agent-frontend/src/services/resultSet.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 结果集数据结构 + */ +export interface ResultSetData { + column: string[]; + data: Array>; + errorMsg?: string; +} + +/** + * 分页配置 + */ +export interface PaginationConfig { + currentPage: number; + pageSize: number; + total: number; +} + +/** + * 结果集显示配置 + */ +export interface ResultSetDisplayConfig { + showSqlResults: boolean; + pageSize: number; +} diff --git a/spring-ai-alibaba-data-agent-frontend/src/views/AgentRun.vue b/spring-ai-alibaba-data-agent-frontend/src/views/AgentRun.vue index e3d26e0c5..c6b85e97a 100644 --- a/spring-ai-alibaba-data-agent-frontend/src/views/AgentRun.vue +++ b/spring-ai-alibaba-data-agent-frontend/src/views/AgentRun.vue @@ -149,6 +149,25 @@ 自动Scroll +
+ 显示SQL结果 + + + +
+
+ 每页数量 + + + + + + + +
@@ -201,10 +220,19 @@ TextType, } from '@/services/graph'; import { type Agent } from '@/services/agent'; + import { type ResultSetData, type ResultSetDisplayConfig } from '@/services/resultSet'; import HumanFeedback from '@/components/run/HumanFeedback.vue'; import ChatSessionSidebar from '@/components/run/ChatSessionSidebar.vue'; // todo: 添加预设问题子件 + // 扩展Window接口以包含自定义方法 + declare global { + interface Window { + copyTextToClipboard: (btn: HTMLElement) => void; + handleResultSetPagination: (btn: HTMLElement, direction: 'prev' | 'next') => void; + } + } + export default defineComponent({ name: 'AgentRun', components: { @@ -216,6 +244,65 @@ HumanFeedback, ChatSessionSidebar, }, + created() { + window.copyTextToClipboard = btn => { + const text = btn.previousElementSibling.textContent; + const originalText = btn.textContent; + + navigator.clipboard + .writeText(text) + .then(() => { + btn.textContent = '已复制!'; + setTimeout(() => { + btn.textContent = originalText; + }, 3000); + }) + .catch(() => { + btn.textContent = '复制失败'; + setTimeout(() => { + btn.textContent = originalText; + }, 3000); + }); + }; + + // 结果集翻页事件处理 + window.handleResultSetPagination = (btn: HTMLElement, direction: 'prev' | 'next') => { + const container = btn.closest('.result-set-container'); + if (!container) return; + + const currentPageElement = container.querySelector('.result-set-current-page'); + const prevBtn = container.querySelector('.result-set-pagination-prev') as HTMLButtonElement; + const nextBtn = container.querySelector('.result-set-pagination-next') as HTMLButtonElement; + const pages = container.querySelectorAll('.result-set-page'); + + if (!currentPageElement || !prevBtn || !nextBtn || pages.length === 0) return; + + let currentPage = parseInt(currentPageElement.textContent || '1'); + const totalPages = pages.length; + + if (direction === 'prev' && currentPage > 1) { + currentPage--; + } else if (direction === 'next' && currentPage < totalPages) { + currentPage++; + } + + // 更新页面显示 + pages.forEach((page: Element) => { + page.classList.remove('result-set-page-active'); + }); + const targetPage = container.querySelector(`.result-set-page[data-page="${currentPage}"]`); + if (targetPage) { + targetPage.classList.add('result-set-page-active'); + } + + // 更新页码显示 + currentPageElement.textContent = currentPage.toString(); + + // 更新按钮状态 + prevBtn.disabled = currentPage === 1; + nextBtn.disabled = currentPage === totalPages; + }; + }, setup() { const route = useRoute(); @@ -253,6 +340,12 @@ const showHumanFeedback = ref(false); const lastRequest = ref(null); + // 结果集显示配置 + const resultSetDisplayConfig = ref({ + showSqlResults: false, + pageSize: 20, + }); + const agentId = computed(() => route.params.id as string); const loadAgent = async () => { @@ -573,7 +666,7 @@ // 使用 highlight.js 进行代码高亮 const language = node[idx].textType.toLowerCase(); const highlighted = hljs.highlight(pre, { language }); - content += `
${highlighted.value}
`; + content += `
${language}
${highlighted.value}
`; } catch (error) { // 如果高亮失败,返回原始代码 content += `
${pre}
`; @@ -583,6 +676,44 @@ } else { break; } + } else if (node[idx].textType === TextType.RESULT_SET) { + // 渲染结果集 + if (!resultSetDisplayConfig.value.showSqlResults) { + // 如果用户关闭了显示SQL结果,直接忽略这个节点 + continue; + } + + try { + // 解析JSON字符串 + const resultSetData: ResultSetData = JSON.parse(node[idx].text); + + // 检查是否有错误信息 + if (resultSetData.errorMsg) { + content += `
错误: ${resultSetData.errorMsg}
`; + continue; + } + + // 检查数据是否为空 + if ( + !resultSetData.column || + resultSetData.column.length === 0 || + !resultSetData.data || + resultSetData.data.length === 0 + ) { + content += `
查询结果为空
`; + continue; + } + + // 生成表格HTML + const tableHtml = generateResultSetTable( + resultSetData, + resultSetDisplayConfig.value.pageSize, + ); + content += tableHtml; + } catch (error) { + console.error('解析结果集JSON失败:', error); + content += `
解析结果集数据失败: ${error.message}
`; + } } else { console.warn(`不支持的 textType: ${node[idx].textType}`); content += node[idx].text; @@ -627,6 +758,61 @@ await sendGraphRequest(newRequest, rejectedPlan); }; + // 生成结果集表格HTML + const generateResultSetTable = (resultSetData: ResultSetData, pageSize: number): string => { + const columns = resultSetData.column || []; + const allData = resultSetData.data || []; + const total = allData.length; + + // 分页逻辑 - 生成所有页面的HTML,通过CSS控制显示 + const totalPages = Math.ceil(total / pageSize); + + let tableHtml = `
查询结果 (共 ${total} 条记录)
第 1 页,共 ${totalPages} 页
`; + + // 生成所有页面的表格 + for (let page = 1; page <= totalPages; page++) { + const startIndex = (page - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, total); + const currentPageData = allData.slice(startIndex, endIndex); + + tableHtml += `
`; + + // 添加表头 + columns.forEach(column => { + tableHtml += ``; + }); + + tableHtml += ``; + + // 添加表格数据 + if (currentPageData.length === 0) { + tableHtml += ``; + } else { + currentPageData.forEach(row => { + tableHtml += ``; + columns.forEach(column => { + const value = row[column] || ''; + tableHtml += ``; + }); + tableHtml += ``; + }); + } + + tableHtml += `
${escapeHtml(column)}
暂无数据
${escapeHtml(value)}
`; + } + + tableHtml += `
`; + + return tableHtml; + }; + + // HTML转义函数 + const escapeHtml = (text: string): string => { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }; + // 生命周期 onMounted(async () => { await loadAgent(); @@ -645,6 +831,7 @@ agentId, showHumanFeedback, lastRequest, + resultSetDisplayConfig, selectSession, sendMessage, formatMessageContent, @@ -937,3 +1124,143 @@ } } + +