From ae2df0c35e72fe71cf2926d4e690c52b8f5bb318 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 29 Apr 2026 17:28:53 +0800 Subject: [PATCH] fix(web): style of batch test --- .../evaluation/__tests__/index.spec.tsx | 123 +++++++++++++++--- .../batch-test-panel/history-tab.tsx | 2 +- .../components/batch-test-panel/index.tsx | 18 +-- .../batch-test-panel/input-fields-tab.tsx | 1 + .../input-fields-requirements.tsx | 8 +- .../input-fields/input-fields-utils.ts | 28 ++++ .../use-published-input-fields.ts | 6 +- web/i18n/en-US/evaluation.json | 1 + web/i18n/zh-Hans/evaluation.json | 1 + 9 files changed, 158 insertions(+), 30 deletions(-) diff --git a/web/app/components/evaluation/__tests__/index.spec.tsx b/web/app/components/evaluation/__tests__/index.spec.tsx index 6b3546b614..b85a4ac924 100644 --- a/web/app/components/evaluation/__tests__/index.spec.tsx +++ b/web/app/components/evaluation/__tests__/index.spec.tsx @@ -12,6 +12,7 @@ const mockUseEvaluationConfig = vi.hoisted(() => vi.fn()) const mockUseSaveEvaluationConfigMutation = vi.hoisted(() => vi.fn()) const mockUseStartEvaluationRunMutation = vi.hoisted(() => vi.fn()) const mockUsePublishedPipelineInfo = vi.hoisted(() => vi.fn()) +const mockUseSnippetPublishedWorkflow = vi.hoisted(() => vi.fn()) vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useModelList: () => ({ @@ -86,23 +87,7 @@ vi.mock('@/service/use-workflow', () => ({ })) vi.mock('@/service/use-snippet-workflows', () => ({ - useSnippetPublishedWorkflow: () => ({ - data: { - graph: { - nodes: [{ - id: 'start', - data: { - type: 'start', - variables: [{ - variable: 'query', - type: 'text-input', - }], - }, - }], - }, - }, - isLoading: false, - }), + useSnippetPublishedWorkflow: (...args: unknown[]) => mockUseSnippetPublishedWorkflow(...args), })) const renderWithQueryClient = (ui: ReactNode) => { @@ -199,6 +184,24 @@ describe('Evaluation', () => { }, }, }) + mockUseSnippetPublishedWorkflow.mockReturnValue({ + data: { + graph: { + nodes: [{ + id: 'start', + data: { + type: 'start', + variables: [{ + variable: 'query', + type: 'text-input', + }], + }, + }], + }, + input_fields: [], + }, + isLoading: false, + }) mockUpload.mockResolvedValue({ id: 'uploaded-file-id', name: 'evaluation.csv', @@ -305,6 +308,92 @@ describe('Evaluation', () => { expect(resetButton).toBeDisabled() }) + it('should hide the batch config warning when judge model and metrics are configured', () => { + const resourceType = 'apps' + const resourceId = 'app-batch-configured' + const store = useEvaluationStore.getState() + + act(() => { + store.ensureResource(resourceType, resourceId) + store.setJudgeModel(resourceType, resourceId, 'openai::gpt-4o-mini') + store.addBuiltinMetric(resourceType, resourceId, 'faithfulness', [ + { node_id: 'node-faithfulness', title: 'Retriever Node', type: 'retriever' }, + ]) + }) + + renderWithQueryClient() + + expect(screen.queryByText('evaluation.batch.noticeDescription')).not.toBeInTheDocument() + }) + + it('should use published snippet input fields for snippet batch templates', () => { + mockUseSnippetPublishedWorkflow.mockReturnValue({ + data: { + graph: { + nodes: [{ + id: 'start', + data: { + type: 'start', + variables: [{ + variable: 'graph_only', + type: 'text-input', + }], + }, + }], + }, + input_fields: [ + { + label: 'Snippet Topic', + variable: 'snippet_topic', + type: 'text-input', + required: true, + }, + { + label: 'Need Summary', + variable: 'need_summary', + type: 'checkbox', + required: false, + }, + ], + }, + isLoading: false, + }) + + renderWithQueryClient() + + expect(mockUseSnippetPublishedWorkflow).toHaveBeenCalledWith('snippet-fields') + expect(screen.getByText('snippet_topic')).toBeInTheDocument() + expect(screen.getByText('need_summary')).toBeInTheDocument() + expect(screen.queryByText('graph_only')).not.toBeInTheDocument() + }) + + it('should show snippet-specific empty input fields copy', () => { + mockUseSnippetPublishedWorkflow.mockReturnValue({ + data: { + graph: { + nodes: [{ + id: 'start', + data: { + type: 'start', + variables: [{ + variable: 'graph_only', + type: 'text-input', + }], + }, + }], + }, + input_fields: [], + }, + isLoading: false, + }) + + renderWithQueryClient() + + expect(screen.getByText('evaluation.batch.noSnippetInputFields')).toBeInTheDocument() + expect(screen.queryByText('evaluation.batch.noInputFields')).not.toBeInTheDocument() + expect(screen.queryByText('graph_only')).not.toBeInTheDocument() + }) + it('should hide the value row for empty operators', () => { const resourceType = 'apps' const resourceId = 'app-2' diff --git a/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx b/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx index 732b4df76c..0819ff2137 100644 --- a/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx @@ -181,7 +181,7 @@ const HistoryTab = ({ {!isInitialLoading && records.length === 0 && ( -
+
{t('history.empty')}
)} diff --git a/web/app/components/evaluation/components/batch-test-panel/index.tsx b/web/app/components/evaluation/components/batch-test-panel/index.tsx index 8b4d9ca98a..2de6aee712 100644 --- a/web/app/components/evaluation/components/batch-test-panel/index.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/index.tsx @@ -22,7 +22,7 @@ const BatchTestPanel = ({ const resource = useEvaluationResource(resourceType, resourceId) const setBatchTab = useEvaluationStore(state => state.setBatchTab) const isRunnable = isEvaluationRunnable(resource) - const isPanelReady = !!resource.judgeModelId && resource.metrics.length > 0 + const hasBatchConfig = !!resource.judgeModelId && resource.metrics.length > 0 return (
@@ -31,12 +31,14 @@ const BatchTestPanel = ({
{t('batch.title')}
{t('batch.description')}
-
-
-
+ )}
@@ -56,12 +58,12 @@ const BatchTestPanel = ({ ))}
-
+
{resource.activeBatchTab === 'input-fields' && ( )} diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx b/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx index 5cff6e3dc0..d6168655a5 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx @@ -33,6 +33,7 @@ const InputFieldsTab = ({ return (
diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx index 83201ea5a7..2169bd6464 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx @@ -1,16 +1,22 @@ +import type { EvaluationResourceType } from '../../../types' import type { InputField } from './input-fields-utils' import { useTranslation } from 'react-i18next' type InputFieldsRequirementsProps = { + resourceType: EvaluationResourceType inputFields: InputField[] isLoading: boolean } const InputFieldsRequirements = ({ + resourceType, inputFields, isLoading, }: InputFieldsRequirementsProps) => { const { t } = useTranslation('evaluation') + const emptyDescription = resourceType === 'snippets' + ? t('batch.noSnippetInputFields') + : t('batch.noInputFields') return (
@@ -24,7 +30,7 @@ const InputFieldsRequirements = ({ )} {!isLoading && inputFields.length === 0 && (
- {t('batch.noInputFields')} + {emptyDescription}
)} {!isLoading && inputFields.map(field => ( diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts index 5a71b81d06..ce21045e13 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts @@ -1,7 +1,9 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import type { InputVar, Node } from '@/app/components/workflow/types' +import type { SnippetInputField } from '@/types/snippet' import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' +import { PipelineInputVarType } from '@/models/pipeline' export type InputField = { name: string @@ -27,6 +29,32 @@ export const getStartNodeInputFields = (nodes?: Node[]): InputField[] => { })) } +const PIPELINE_INPUT_VAR_TYPE_TO_FIELD_TYPE: Record = { + [PipelineInputVarType.textInput]: 'string', + [PipelineInputVarType.paragraph]: 'string', + [PipelineInputVarType.select]: 'string', + [PipelineInputVarType.number]: 'number', + [PipelineInputVarType.singleFile]: 'file', + [PipelineInputVarType.multiFiles]: 'array[file]', + [PipelineInputVarType.checkbox]: 'boolean', +} + +export const getSnippetInputFields = (fields?: SnippetInputField[]): InputField[] => { + if (!Array.isArray(fields)) + return [] + + return fields + .filter((field): field is SnippetInputField & { variable: string } => + typeof field.variable === 'string' && !!field.variable, + ) + .map(field => ({ + name: field.variable, + type: typeof field.type === 'string' && field.type in PIPELINE_INPUT_VAR_TYPE_TO_FIELD_TYPE + ? PIPELINE_INPUT_VAR_TYPE_TO_FIELD_TYPE[field.type as PipelineInputVarType] + : 'string', + })) +} + const escapeCsvCell = (value: string) => { if (!/[",\n\r]/.test(value)) return value diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts b/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts index a319603026..d56507c3ac 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts @@ -2,7 +2,7 @@ import type { EvaluationResourceType } from '../../../types' import { useMemo } from 'react' import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows' import { useAppWorkflow } from '@/service/use-workflow' -import { getGraphNodes, getStartNodeInputFields } from './input-fields-utils' +import { getSnippetInputFields, getStartNodeInputFields } from './input-fields-utils' export const usePublishedInputFields = ( resourceType: EvaluationResourceType, @@ -16,10 +16,10 @@ export const usePublishedInputFields = ( return getStartNodeInputFields(currentAppWorkflow?.graph.nodes) if (resourceType === 'snippets') - return getStartNodeInputFields(getGraphNodes(currentSnippetWorkflow?.graph)) + return getSnippetInputFields(currentSnippetWorkflow?.input_fields) return [] - }, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.graph, resourceType]) + }, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.input_fields, resourceType]) return { inputFields, diff --git a/web/i18n/en-US/evaluation.json b/web/i18n/en-US/evaluation.json index dfdde63e26..e5d5ff790e 100644 --- a/web/i18n/en-US/evaluation.json +++ b/web/i18n/en-US/evaluation.json @@ -6,6 +6,7 @@ "batch.fileRequired": "Upload an evaluation dataset file before running the test.", "batch.loadingInputFields": "Loading input fields...", "batch.noInputFields": "No published start node input fields found.", + "batch.noSnippetInputFields": "No published snippet input fields found.", "batch.noticeDescription": "Configuration incomplete. Select the Judge Model and Metrics on the left to generate your batch test template.", "batch.noticeTitle": "Quick start", "batch.removeUploadedFile": "Remove uploaded file", diff --git a/web/i18n/zh-Hans/evaluation.json b/web/i18n/zh-Hans/evaluation.json index 7ca9b76874..d2c465c04e 100644 --- a/web/i18n/zh-Hans/evaluation.json +++ b/web/i18n/zh-Hans/evaluation.json @@ -6,6 +6,7 @@ "batch.fileRequired": "请先上传评估数据集文件,再运行测试。", "batch.loadingInputFields": "正在加载输入字段...", "batch.noInputFields": "未找到已发布 Start 节点的输入字段。", + "batch.noSnippetInputFields": "未找到已发布的片段输入字段。", "batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。", "batch.noticeTitle": "快速开始", "batch.removeUploadedFile": "移除已上传文件",