mirror of
https://github.com/langgenius/dify.git
synced 2026-05-07 02:46:32 +08:00
fix(web): style of batch test
This commit is contained in:
parent
dacc7fc740
commit
ae2df0c35e
@ -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'
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -33,6 +33,7 @@ const InputFieldsTab = ({
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<InputFieldsRequirements
|
||||
resourceType={resourceType}
|
||||
inputFields={inputFields}
|
||||
isLoading={isInputFieldsLoading}
|
||||
/>
|
||||
|
||||
@ -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 => (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
|
||||
"batch.loadingInputFields": "正在加载输入字段...",
|
||||
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
|
||||
"batch.noSnippetInputFields": "未找到已发布的片段输入字段。",
|
||||
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
|
||||
"batch.noticeTitle": "快速开始",
|
||||
"batch.removeUploadedFile": "移除已上传文件",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user