mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +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 mockUseSaveEvaluationConfigMutation = vi.hoisted(() => vi.fn())
|
||||||
const mockUseStartEvaluationRunMutation = vi.hoisted(() => vi.fn())
|
const mockUseStartEvaluationRunMutation = vi.hoisted(() => vi.fn())
|
||||||
const mockUsePublishedPipelineInfo = 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', () => ({
|
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||||
useModelList: () => ({
|
useModelList: () => ({
|
||||||
@ -86,23 +87,7 @@ vi.mock('@/service/use-workflow', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/use-snippet-workflows', () => ({
|
vi.mock('@/service/use-snippet-workflows', () => ({
|
||||||
useSnippetPublishedWorkflow: () => ({
|
useSnippetPublishedWorkflow: (...args: unknown[]) => mockUseSnippetPublishedWorkflow(...args),
|
||||||
data: {
|
|
||||||
graph: {
|
|
||||||
nodes: [{
|
|
||||||
id: 'start',
|
|
||||||
data: {
|
|
||||||
type: 'start',
|
|
||||||
variables: [{
|
|
||||||
variable: 'query',
|
|
||||||
type: 'text-input',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isLoading: false,
|
|
||||||
}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const renderWithQueryClient = (ui: ReactNode) => {
|
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({
|
mockUpload.mockResolvedValue({
|
||||||
id: 'uploaded-file-id',
|
id: 'uploaded-file-id',
|
||||||
name: 'evaluation.csv',
|
name: 'evaluation.csv',
|
||||||
@ -305,6 +308,92 @@ describe('Evaluation', () => {
|
|||||||
expect(resetButton).toBeDisabled()
|
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', () => {
|
it('should hide the value row for empty operators', () => {
|
||||||
const resourceType = 'apps'
|
const resourceType = 'apps'
|
||||||
const resourceId = 'app-2'
|
const resourceId = 'app-2'
|
||||||
|
|||||||
@ -181,7 +181,7 @@ const HistoryTab = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{!isInitialLoading && records.length === 0 && (
|
{!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')}
|
{t('history.empty')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const BatchTestPanel = ({
|
|||||||
const resource = useEvaluationResource(resourceType, resourceId)
|
const resource = useEvaluationResource(resourceType, resourceId)
|
||||||
const setBatchTab = useEvaluationStore(state => state.setBatchTab)
|
const setBatchTab = useEvaluationStore(state => state.setBatchTab)
|
||||||
const isRunnable = isEvaluationRunnable(resource)
|
const isRunnable = isEvaluationRunnable(resource)
|
||||||
const isPanelReady = !!resource.judgeModelId && resource.metrics.length > 0
|
const hasBatchConfig = !!resource.judgeModelId && resource.metrics.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col bg-background-default">
|
<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="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 className="mt-1 system-sm-regular text-text-tertiary">{t('batch.description')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!hasBatchConfig && (
|
||||||
<div className="mt-4 rounded-xl border border-divider-subtle bg-components-card-bg p-3">
|
<div className="mt-4 rounded-xl border border-divider-subtle bg-components-card-bg p-3">
|
||||||
<div className="flex items-start gap-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" />
|
<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 className="system-xs-regular text-text-tertiary">{t('batch.noticeDescription')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-divider-subtle px-6">
|
<div className="border-b border-divider-subtle px-6">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@ -56,12 +58,12 @@ const BatchTestPanel = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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' && (
|
{resource.activeBatchTab === 'input-fields' && (
|
||||||
<InputFieldsTab
|
<InputFieldsTab
|
||||||
resourceType={resourceType}
|
resourceType={resourceType}
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
isPanelReady={isPanelReady}
|
isPanelReady={hasBatchConfig}
|
||||||
isRunnable={isRunnable}
|
isRunnable={isRunnable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ const InputFieldsTab = ({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<InputFieldsRequirements
|
<InputFieldsRequirements
|
||||||
|
resourceType={resourceType}
|
||||||
inputFields={inputFields}
|
inputFields={inputFields}
|
||||||
isLoading={isInputFieldsLoading}
|
isLoading={isInputFieldsLoading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
|
import type { EvaluationResourceType } from '../../../types'
|
||||||
import type { InputField } from './input-fields-utils'
|
import type { InputField } from './input-fields-utils'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type InputFieldsRequirementsProps = {
|
type InputFieldsRequirementsProps = {
|
||||||
|
resourceType: EvaluationResourceType
|
||||||
inputFields: InputField[]
|
inputFields: InputField[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputFieldsRequirements = ({
|
const InputFieldsRequirements = ({
|
||||||
|
resourceType,
|
||||||
inputFields,
|
inputFields,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: InputFieldsRequirementsProps) => {
|
}: InputFieldsRequirementsProps) => {
|
||||||
const { t } = useTranslation('evaluation')
|
const { t } = useTranslation('evaluation')
|
||||||
|
const emptyDescription = resourceType === 'snippets'
|
||||||
|
? t('batch.noSnippetInputFields')
|
||||||
|
: t('batch.noInputFields')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -24,7 +30,7 @@ const InputFieldsRequirements = ({
|
|||||||
)}
|
)}
|
||||||
{!isLoading && inputFields.length === 0 && (
|
{!isLoading && inputFields.length === 0 && (
|
||||||
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
|
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
|
||||||
{t('batch.noInputFields')}
|
{emptyDescription}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && inputFields.map(field => (
|
{!isLoading && inputFields.map(field => (
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||||
import type { InputVar, Node } from '@/app/components/workflow/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 { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||||
|
import { PipelineInputVarType } from '@/models/pipeline'
|
||||||
|
|
||||||
export type InputField = {
|
export type InputField = {
|
||||||
name: string
|
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) => {
|
const escapeCsvCell = (value: string) => {
|
||||||
if (!/[",\n\r]/.test(value))
|
if (!/[",\n\r]/.test(value))
|
||||||
return value
|
return value
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { EvaluationResourceType } from '../../../types'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows'
|
import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows'
|
||||||
import { useAppWorkflow } from '@/service/use-workflow'
|
import { useAppWorkflow } from '@/service/use-workflow'
|
||||||
import { getGraphNodes, getStartNodeInputFields } from './input-fields-utils'
|
import { getSnippetInputFields, getStartNodeInputFields } from './input-fields-utils'
|
||||||
|
|
||||||
export const usePublishedInputFields = (
|
export const usePublishedInputFields = (
|
||||||
resourceType: EvaluationResourceType,
|
resourceType: EvaluationResourceType,
|
||||||
@ -16,10 +16,10 @@ export const usePublishedInputFields = (
|
|||||||
return getStartNodeInputFields(currentAppWorkflow?.graph.nodes)
|
return getStartNodeInputFields(currentAppWorkflow?.graph.nodes)
|
||||||
|
|
||||||
if (resourceType === 'snippets')
|
if (resourceType === 'snippets')
|
||||||
return getStartNodeInputFields(getGraphNodes(currentSnippetWorkflow?.graph))
|
return getSnippetInputFields(currentSnippetWorkflow?.input_fields)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.graph, resourceType])
|
}, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.input_fields, resourceType])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputFields,
|
inputFields,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"batch.fileRequired": "Upload an evaluation dataset file before running the test.",
|
"batch.fileRequired": "Upload an evaluation dataset file before running the test.",
|
||||||
"batch.loadingInputFields": "Loading input fields...",
|
"batch.loadingInputFields": "Loading input fields...",
|
||||||
"batch.noInputFields": "No published start node input fields found.",
|
"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.noticeDescription": "Configuration incomplete. Select the Judge Model and Metrics on the left to generate your batch test template.",
|
||||||
"batch.noticeTitle": "Quick start",
|
"batch.noticeTitle": "Quick start",
|
||||||
"batch.removeUploadedFile": "Remove uploaded file",
|
"batch.removeUploadedFile": "Remove uploaded file",
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
|
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
|
||||||
"batch.loadingInputFields": "正在加载输入字段...",
|
"batch.loadingInputFields": "正在加载输入字段...",
|
||||||
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
|
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
|
||||||
|
"batch.noSnippetInputFields": "未找到已发布的片段输入字段。",
|
||||||
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
|
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
|
||||||
"batch.noticeTitle": "快速开始",
|
"batch.noticeTitle": "快速开始",
|
||||||
"batch.removeUploadedFile": "移除已上传文件",
|
"batch.removeUploadedFile": "移除已上传文件",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user