From a4763a83c664ecf1bac4ce19bde4df131b3d09f7 Mon Sep 17 00:00:00 2001 From: zxuexingzhijie <2762006003@qq.com> Date: Mon, 27 Jul 2026 11:19:34 +0800 Subject: [PATCH] fix(ui): render mixed workflow code blocks --- .../components/chat/ChatWorkflowTimeline.vue | 43 +++++++------- .../app/utils/workflowTimeline.test.ts | 56 +++++++++++++++++-- .../app/utils/workflowTimeline.ts | 38 +++++++++++-- 3 files changed, 109 insertions(+), 28 deletions(-) diff --git a/data-agent-frontend-nuxt/app/components/chat/ChatWorkflowTimeline.vue b/data-agent-frontend-nuxt/app/components/chat/ChatWorkflowTimeline.vue index dd998995f..4ac8ce3ef 100644 --- a/data-agent-frontend-nuxt/app/components/chat/ChatWorkflowTimeline.vue +++ b/data-agent-frontend-nuxt/app/components/chat/ChatWorkflowTimeline.vue @@ -81,17 +81,25 @@ > 报告已生成完毕,查看下方报告卡片 - -
- -
+ +
+ +
(null); const { renderECharts } = useEchartsRenderer(); -const CODE_TEXT_TYPES = new Set(['SQL', 'PYTHON', 'JSON']); - -function isPureCodeBlock(block: GraphNodeResponse[]): boolean { - return ( - block.length > 0 && block.every((n) => CODE_TEXT_TYPES.has(n.textType)) - ); -} - const SANITIZE_OPTIONS = { ADD_TAGS: ['pre', 'code'], ADD_ATTR: ['class'], diff --git a/data-agent-frontend-nuxt/app/utils/workflowTimeline.test.ts b/data-agent-frontend-nuxt/app/utils/workflowTimeline.test.ts index bd575c5ee..c7b3c176c 100644 --- a/data-agent-frontend-nuxt/app/utils/workflowTimeline.test.ts +++ b/data-agent-frontend-nuxt/app/utils/workflowTimeline.test.ts @@ -15,11 +15,11 @@ */ import { describe, expect, it } from 'vitest'; +import { TextType, type GraphNodeResponse } from '../services/graph/index'; import { - TextType, - type GraphNodeResponse, -} from '../services/graph/index'; -import { groupWorkflowTimeline } from './workflowTimeline'; + groupWorkflowTimeline, + segmentWorkflowContent, +} from './workflowTimeline'; function event( nodeName: string, @@ -88,3 +88,51 @@ describe('groupWorkflowTimeline', () => { expect(groups[0]?.items).toHaveLength(2); }); }); + +describe('segmentWorkflowContent', () => { + it('keeps streamed SQL as a code segment between status messages', () => { + const segments = segmentWorkflowContent([ + event('SqlGenerateNode', '开始生成SQL...\n'), + event('SqlGenerateNode', 'select', { textType: TextType.SQL }), + event('SqlGenerateNode', ' * from users', { + textType: TextType.SQL, + }), + event('SqlGenerateNode', 'SQL生成完成\n'), + ]); + + expect(segments.map((segment) => segment.kind)).toEqual([ + 'text', + 'code', + 'text', + ]); + expect(segments[1]?.items.map((item) => item.text).join('')).toBe( + 'select * from users', + ); + }); + + it('keeps executed SQL separate from surrounding execution text', () => { + const segments = segmentWorkflowContent([ + event('SqlExecuteNode', '开始执行SQL...\n'), + event('SqlExecuteNode', '执行SQL查询:\n'), + event('SqlExecuteNode', 'select 1\n', { textType: TextType.SQL }), + event('SqlExecuteNode', '执行SQL完成\n'), + event('SqlExecuteNode', 'SQL查询结果:\n'), + ]); + + expect(segments).toHaveLength(3); + expect(segments[0]?.items).toHaveLength(2); + expect(segments[1]?.kind).toBe('code'); + expect(segments[1]?.items[0]?.textType).toBe(TextType.SQL); + expect(segments[2]?.items).toHaveLength(2); + }); + + it('does not merge adjacent code written in different languages', () => { + const segments = segmentWorkflowContent([ + event('MixedNode', 'select 1', { textType: TextType.SQL }), + event('MixedNode', 'print(1)', { textType: TextType.PYTHON }), + ]); + + expect(segments).toHaveLength(2); + expect(segments.every((segment) => segment.kind === 'code')).toBe(true); + }); +}); diff --git a/data-agent-frontend-nuxt/app/utils/workflowTimeline.ts b/data-agent-frontend-nuxt/app/utils/workflowTimeline.ts index 000d68d31..f3804347e 100644 --- a/data-agent-frontend-nuxt/app/utils/workflowTimeline.ts +++ b/data-agent-frontend-nuxt/app/utils/workflowTimeline.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - TextType, - type GraphNodeResponse, -} from '../services/graph/index'; +import { TextType, type GraphNodeResponse } from '../services/graph/index'; export interface WorkflowStepGroup { stepId: string; @@ -26,6 +23,39 @@ export interface WorkflowStepGroup { items: GraphNodeResponse[]; } +export interface WorkflowContentSegment { + kind: 'code' | 'text'; + items: GraphNodeResponse[]; +} + +const CODE_TEXT_TYPES = new Set([TextType.SQL, TextType.PYTHON, TextType.JSON]); + +/** + * Splits one node execution into renderable text and code segments. A workflow + * step normally contains status text before and after streamed code, so treating + * the whole step as plain text would discard the backend's SQL/Python/JSON type. + */ +export function segmentWorkflowContent( + items: GraphNodeResponse[], +): WorkflowContentSegment[] { + const segments: WorkflowContentSegment[] = []; + + for (const item of items) { + const kind = CODE_TEXT_TYPES.has(item.textType) ? 'code' : 'text'; + const previous = segments.at(-1); + const sameCodeType = + kind !== 'code' || previous?.items[0]?.textType === item.textType; + + if (previous?.kind === kind && sameCodeType) { + previous.items.push(item); + } else { + segments.push({ kind, items: [item] }); + } + } + + return segments; +} + /** * Groups workflow events by backend step identity. The fallback branch keeps persisted * timelines from older versions readable by attaching a result-only block to the