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')}
-
-
-
-
{t('batch.noticeDescription')}
+ {!hasBatchConfig && (
+
+
+
+
{t('batch.noticeDescription')}
+
-
+ )}
@@ -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": "移除已上传文件",