fix(web): style of batch test

This commit is contained in:
JzoNg 2026-04-29 17:28:53 +08:00
parent dacc7fc740
commit ae2df0c35e
9 changed files with 158 additions and 30 deletions

View File

@ -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(<Evaluation resourceType={resourceType} resourceId={resourceId} />)
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(<Evaluation resourceType="snippets" resourceId="snippet-fields" />)
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(<Evaluation resourceType="snippets" resourceId="snippet-empty-fields" />)
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'

View File

@ -181,7 +181,7 @@ const HistoryTab = ({
</tbody>
</table>
{!isInitialLoading && records.length === 0 && (
<div className="rounded-2xl border border-dashed border-divider-subtle px-4 py-10 text-center system-sm-regular text-text-tertiary">
<div className="mt-4 rounded-2xl border border-dashed border-divider-subtle px-4 py-10 text-center system-sm-regular text-text-tertiary">
{t('history.empty')}
</div>
)}

View File

@ -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 (
<div className="flex h-full min-h-0 flex-col bg-background-default">
@ -31,12 +31,14 @@ const BatchTestPanel = ({
<div className="system-xl-semibold text-text-primary">{t('batch.title')}</div>
<div className="mt-1 system-sm-regular text-text-tertiary">{t('batch.description')}</div>
</div>
<div className="mt-4 rounded-xl border border-divider-subtle bg-components-card-bg p-3">
<div className="flex items-start gap-3">
<span aria-hidden="true" className="mt-0.5 i-ri-alert-fill h-4 w-4 shrink-0 text-text-warning" />
<div className="system-xs-regular text-text-tertiary">{t('batch.noticeDescription')}</div>
{!hasBatchConfig && (
<div className="mt-4 rounded-xl border border-divider-subtle bg-components-card-bg p-3">
<div className="flex items-start gap-3">
<span aria-hidden="true" className="mt-0.5 i-ri-alert-fill h-4 w-4 shrink-0 text-text-warning" />
<div className="system-xs-regular text-text-tertiary">{t('batch.noticeDescription')}</div>
</div>
</div>
</div>
)}
</div>
<div className="border-b border-divider-subtle px-6">
<div className="flex gap-4">
@ -56,12 +58,12 @@ const BatchTestPanel = ({
))}
</div>
</div>
<div className={cn('min-h-0 flex-1 overflow-y-auto px-6 py-4', !isPanelReady && 'opacity-50')}>
<div className={cn('min-h-0 flex-1 overflow-y-auto px-6 py-4', !hasBatchConfig && 'opacity-50')}>
{resource.activeBatchTab === 'input-fields' && (
<InputFieldsTab
resourceType={resourceType}
resourceId={resourceId}
isPanelReady={isPanelReady}
isPanelReady={hasBatchConfig}
isRunnable={isRunnable}
/>
)}

View File

@ -33,6 +33,7 @@ const InputFieldsTab = ({
return (
<div className="space-y-5">
<InputFieldsRequirements
resourceType={resourceType}
inputFields={inputFields}
isLoading={isInputFieldsLoading}
/>

View File

@ -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 (
<div>
@ -24,7 +30,7 @@ const InputFieldsRequirements = ({
)}
{!isLoading && inputFields.length === 0 && (
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
{t('batch.noInputFields')}
{emptyDescription}
</div>
)}
{!isLoading && inputFields.map(field => (

View File

@ -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, string> = {
[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

View File

@ -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,

View File

@ -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",

View File

@ -6,6 +6,7 @@
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
"batch.loadingInputFields": "正在加载输入字段...",
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
"batch.noSnippetInputFields": "未找到已发布的片段输入字段。",
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
"batch.noticeTitle": "快速开始",
"batch.removeUploadedFile": "移除已上传文件",