diff --git a/easyflow-ui-admin/app/src/components/json/ShowJson.vue b/easyflow-ui-admin/app/src/components/json/ShowJson.vue index 21bc9145..c9cdfac5 100644 --- a/easyflow-ui-admin/app/src/components/json/ShowJson.vue +++ b/easyflow-ui-admin/app/src/components/json/ShowJson.vue @@ -12,8 +12,8 @@ import 'vue3-json-viewer/dist/vue3-json-viewer.css'; withDefaults( defineProps<{ - value: unknown; plain?: boolean; + value: unknown; }>(), { plain: false, @@ -49,4 +49,8 @@ watch( border: none; border-radius: 0; } + +.res-container :deep(.jv-item.jv-string) { + white-space: pre-wrap; +} diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue index 2ac35632..3ccdbe8e 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue @@ -69,7 +69,7 @@ function makeItem(item: any, index: number) { :src="`${item}`" class="h-3/5 w-full" > -
+
{{ typeof item === 'string' ? item : JSON.stringify(item) }}
diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts index d6c98d5c..269c7802 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { describe, expect, it, vi } from 'vitest'; @@ -44,4 +45,21 @@ describe('workflow final output', () => { expect(wrapper.text()).toContain('复制全部'); expect(wrapper.findAll('button')).toHaveLength(3); }); + + it('renders scalar line breaks in the workflow chat result', async () => { + const wrapper = mount(WorkflowFinalOutput, { + props: { + output: { + output: '问题:你是谁\n答案:你好', + }, + }, + }); + + await nextTick(); + + const textOutput = wrapper.get('.workflow-final-output__text'); + expect(textOutput.text()).toContain('问题:你是谁'); + expect(textOutput.text()).toContain('答案:你好'); + expect(textOutput.find('br').exists()).toBe(true); + }); }); diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts new file mode 100644 index 00000000..70ddc5e2 --- /dev/null +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts @@ -0,0 +1,21 @@ +import { mount } from '@vue/test-utils'; + +import { describe, expect, it } from 'vitest'; + +import ExecResultItem from '../ExecResultItem.vue'; + +describe('execResultItem', () => { + it('preserves line breaks for text results', () => { + const wrapper = mount(ExecResultItem, { + props: { + contentType: 'text', + defName: 'output', + result: ['问题:你是谁\n答案:你好'], + resultCount: 1, + }, + }); + + const textResult = wrapper.get('.whitespace-pre-wrap'); + expect(textResult.text()).toContain('问题:你是谁\n答案:你好'); + }); +}); diff --git a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts index eba1279b..cea25d90 100644 --- a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts +++ b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts @@ -1,7 +1,7 @@ -import {mount} from '@vue/test-utils'; -import {nextTick} from 'vue'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; -import {describe, expect, it, vi} from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import ChatTimeMarkdown from '../ChatTimeMarkdown.vue'; @@ -11,7 +11,21 @@ vi.mock('@easyflow-core/preferences', () => ({ }), })); -describe('ChatTimeMarkdown', () => { +describe('chatTimeMarkdown', () => { + it('renders a single source line break as a visible line break', async () => { + const wrapper = mount(ChatTimeMarkdown, { + props: { + content: '问题:你是谁\n答案:你好', + }, + }); + + await nextTick(); + + expect(wrapper.text()).toContain('问题:你是谁'); + expect(wrapper.text()).toContain('答案:你好'); + expect(wrapper.find('br').exists()).toBe(true); + }); + it('renders mermaid code fences with the shared mermaid block', async () => { const wrapper = mount(ChatTimeMarkdown, { props: { diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte index 54ab0c96..c1df5de7 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte @@ -20,7 +20,9 @@ findBackspaceTokenRange, findTokenRangeAtCursor, flattenParameterCandidates, + formatParamToken, getTokenRanges, + type TokenSyntax, } from '../utils/paramToken'; const { @@ -30,6 +32,7 @@ rows = 3, placeholder = '', disabled = false, + tokenSyntax = 'mustache', class: className = '', style = '', ...rest @@ -40,6 +43,7 @@ rows?: number; placeholder?: string; disabled?: boolean; + tokenSyntax?: TokenSyntax; class?: string; style?: string; [key: string]: any; @@ -169,10 +173,13 @@ }); } - function createTokenDecorations(metaMap: Map): Extension { + function createTokenDecorations( + metaMap: Map, + syntax: TokenSyntax + ): Extension { const buildDecorations = (source: string) => { const decorations = []; - for (const range of getTokenRanges(source)) { + for (const range of getTokenRanges(source, syntax)) { const tokenMeta = metaMap.get(range.key); const displayName = !tokenMeta || tokenMeta.variant === 'invalid' @@ -299,12 +306,15 @@ if (!editorView || disabled) { return; } - insertTextAtSelection(`{{${paramName}}}`); + insertTextAtSelection(formatParamToken(paramName, tokenSyntax)); emitChange(editorView.state.doc.toString()); triggerObject?.hide?.(); } - function moveCursorAcrossToken(direction: 'left' | 'right') { + function moveCursorAcrossToken( + direction: 'left' | 'right', + syntax: TokenSyntax + ) { return (view: EditorView) => { const selection = view.state.selection.main; if (!selection.empty) { @@ -317,11 +327,13 @@ direction === 'left' ? findTokenRangeAtCursor(source, cursor, { includeStart: false, - includeEnd: true + includeEnd: true, + syntax }) : findTokenRangeAtCursor(source, cursor, { includeStart: true, - includeEnd: false + includeEnd: false, + syntax }); if (!range) { @@ -341,13 +353,17 @@ }; } - function deleteTokenBackward(view: EditorView) { + function deleteTokenBackward(view: EditorView, syntax: TokenSyntax) { const selection = view.state.selection.main; if (!selection.empty) { return false; } - const range = findBackspaceTokenRange(view.state.doc.toString(), selection.head); + const range = findBackspaceTokenRange( + view.state.doc.toString(), + selection.head, + syntax + ); if (!range) { return false; } @@ -366,16 +382,21 @@ return true; } - function deleteTokenForward(view: EditorView) { + function deleteTokenForward(view: EditorView, syntax: TokenSyntax) { const selection = view.state.selection.main; if (!selection.empty) { return false; } - const range = findTokenRangeAtCursor(view.state.doc.toString(), selection.head, { - includeStart: true, - includeEnd: false - }); + const range = findTokenRangeAtCursor( + view.state.doc.toString(), + selection.head, + { + includeStart: true, + includeEnd: false, + syntax + } + ); if (!range) { return false; } @@ -398,23 +419,26 @@ return text.replace(/\r\n|\r|\n/g, ' '); } - function createEditorKeymap(singleLine: boolean) { + function createEditorKeymap( + singleLine: boolean, + syntax: TokenSyntax + ) { const entries = [ { key: 'ArrowLeft', - run: moveCursorAcrossToken('left') + run: moveCursorAcrossToken('left', syntax) }, { key: 'ArrowRight', - run: moveCursorAcrossToken('right') + run: moveCursorAcrossToken('right', syntax) }, { key: 'Backspace', - run: deleteTokenBackward + run: (view: EditorView) => deleteTokenBackward(view, syntax) }, { key: 'Delete', - run: deleteTokenForward + run: (view: EditorView) => deleteTokenForward(view, syntax) }, ...defaultKeymap, ...historyKeymap @@ -528,11 +552,11 @@ } }), eventCompartment.of(createDomEventHandlers(isSingleLine)), - keymapCompartment.of(createEditorKeymap(isSingleLine)), + keymapCompartment.of(createEditorKeymap(isSingleLine, tokenSyntax)), editableCompartment.of([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)]), placeholderCompartment.of(placeholder ? cmPlaceholder(placeholder) : []), lineModeCompartment.of(createLineModeExtension(isSingleLine)), - tokenCompartment.of(createTokenDecorations(tokenMetaMap)), + tokenCompartment.of(createTokenDecorations(tokenMetaMap, tokenSyntax)), themeCompartment.of(createEditorTheme(isSingleLine)) ] }) @@ -585,7 +609,9 @@ return; } editorView.dispatch({ - effects: keymapCompartment.reconfigure(createEditorKeymap(isSingleLine)) + effects: keymapCompartment.reconfigure( + createEditorKeymap(isSingleLine, tokenSyntax) + ) }); }); @@ -603,7 +629,9 @@ return; } editorView.dispatch({ - effects: tokenCompartment.reconfigure(createTokenDecorations(tokenMetaMap)) + effects: tokenCompartment.reconfigure( + createTokenDecorations(tokenMetaMap, tokenSyntax) + ) }); }); diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts index 62a02187..8fbce075 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts @@ -170,4 +170,43 @@ describe('ParamTokenEditor', () => { await destroy(); }); + + it('should render and insert Enjoy parameter tokens', async () => { + const onchange = vi.fn(); + const { host, view, destroy } = await renderEditor({ + tokenSyntax: 'enjoy', + value: '申请人:#(申请人)', + parameters: [ + { + name: '申请人', + refType: 'ref', + ref: 'llm.output', + }, + ], + onchange, + }); + + expect(host.querySelector('.param-token-chip-text')?.textContent).toBe( + '申请人', + ); + + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: '', + }, + }); + (host.querySelector('.param-token-item') as HTMLButtonElement).click(); + flushSync(); + + expect(view.state.doc.toString()).toBe('#(申请人)'); + expect(onchange).toHaveBeenCalledWith({ + target: { + value: '#(申请人)', + }, + }); + + await destroy(); + }); }); diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte index 36bf8dc7..eb9fac9e 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte @@ -2,14 +2,17 @@