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