mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
refactor(web): refactor condition group
This commit is contained in:
parent
0438285277
commit
4d1499ef75
@ -117,13 +117,13 @@ describe('Evaluation', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should render time placeholders and hide the value row for empty operators', () => {
|
||||
it('should hide the value row for empty operators', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-2'
|
||||
const store = useEvaluationStore.getState()
|
||||
const config = getEvaluationMockConfig(resourceType)
|
||||
|
||||
const timeField = config.fieldOptions.find(field => field.type === 'time')!
|
||||
const stringField = config.fieldOptions.find(field => field.type === 'string')!
|
||||
let groupId = ''
|
||||
let itemId = ''
|
||||
|
||||
@ -135,8 +135,8 @@ describe('Evaluation', () => {
|
||||
groupId = group.id
|
||||
itemId = group.items[0].id
|
||||
|
||||
store.updateConditionField(resourceType, resourceId, groupId, itemId, timeField.id)
|
||||
store.updateConditionOperator(resourceType, resourceId, groupId, itemId, 'before')
|
||||
store.updateConditionField(resourceType, resourceId, groupId, itemId, stringField.id)
|
||||
store.updateConditionOperator(resourceType, resourceId, groupId, itemId, 'contains')
|
||||
})
|
||||
|
||||
let rerender: ReturnType<typeof render>['rerender']
|
||||
@ -144,14 +144,14 @@ describe('Evaluation', () => {
|
||||
({ rerender } = render(<Evaluation resourceType={resourceType} resourceId={resourceId} />))
|
||||
})
|
||||
|
||||
expect(screen.getByText('evaluation.conditions.selectTime')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('evaluation.conditions.valuePlaceholder')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
store.updateConditionOperator(resourceType, resourceId, groupId, itemId, 'is_empty')
|
||||
rerender(<Evaluation resourceType={resourceType} resourceId={resourceId} />)
|
||||
})
|
||||
|
||||
expect(screen.queryByText('evaluation.conditions.selectTime')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('evaluation.conditions.valuePlaceholder')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the metric no-node empty state', () => {
|
||||
|
||||
@ -123,7 +123,7 @@ describe('evaluation store', () => {
|
||||
expect(getAllowedOperators(resourceType, booleanField.id)).toEqual(['is', 'is_not'])
|
||||
})
|
||||
|
||||
it('should support time fields and clear values for empty operators', () => {
|
||||
it('should clear values for empty operators', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-3'
|
||||
const store = useEvaluationStore.getState()
|
||||
@ -131,15 +131,15 @@ describe('evaluation store', () => {
|
||||
|
||||
store.ensureResource(resourceType, resourceId)
|
||||
|
||||
const timeField = config.fieldOptions.find(field => field.type === 'time')!
|
||||
const stringField = config.fieldOptions.find(field => field.type === 'string')!
|
||||
const item = useEvaluationStore.getState().resources['apps:app-3'].conditions[0].items[0]
|
||||
|
||||
store.updateConditionField(resourceType, resourceId, useEvaluationStore.getState().resources['apps:app-3'].conditions[0].id, item.id, timeField.id)
|
||||
store.updateConditionField(resourceType, resourceId, useEvaluationStore.getState().resources['apps:app-3'].conditions[0].id, item.id, stringField.id)
|
||||
store.updateConditionOperator(resourceType, resourceId, useEvaluationStore.getState().resources['apps:app-3'].conditions[0].id, item.id, 'is_empty')
|
||||
|
||||
const updatedItem = useEvaluationStore.getState().resources['apps:app-3'].conditions[0].items[0]
|
||||
|
||||
expect(getAllowedOperators(resourceType, timeField.id)).toEqual(['is', 'before', 'after', 'is_empty', 'is_not_empty'])
|
||||
expect(getAllowedOperators(resourceType, stringField.id)).toEqual(['contains', 'not_contains', 'is', 'is_not', 'is_empty', 'is_not_empty'])
|
||||
expect(requiresConditionValue('is_empty')).toBe(false)
|
||||
expect(updatedItem.value).toBeNull()
|
||||
})
|
||||
|
||||
@ -5,12 +5,10 @@ import type {
|
||||
EvaluationFieldOption,
|
||||
EvaluationResourceProps,
|
||||
JudgmentConditionGroup,
|
||||
} from '../types'
|
||||
} from '../../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import dayjs from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
Select,
|
||||
@ -22,9 +20,9 @@ import {
|
||||
SelectValue,
|
||||
} from '@/app/components/base/ui/select'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getEvaluationMockConfig } from '../mock'
|
||||
import { getAllowedOperators, requiresConditionValue, useEvaluationStore } from '../store'
|
||||
import { getFieldTypeIconClassName, getOperatorLabel, groupFieldOptions } from '../utils'
|
||||
import { getEvaluationMockConfig } from '../../mock'
|
||||
import { getAllowedOperators, requiresConditionValue, useEvaluationStore } from '../../store'
|
||||
import { getFieldTypeIconClassName, getOperatorLabel, groupFieldOptions } from '../../utils'
|
||||
|
||||
type ConditionFieldLabelProps = {
|
||||
field?: EvaluationFieldOption
|
||||
@ -62,15 +60,15 @@ const ConditionFieldLabel = ({
|
||||
placeholder,
|
||||
}: ConditionFieldLabelProps) => {
|
||||
if (!field)
|
||||
return <span className="px-1 text-components-input-text-placeholder system-sm-regular">{placeholder}</span>
|
||||
return <span className="px-1 system-sm-regular text-components-input-text-placeholder">{placeholder}</span>
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2 px-1">
|
||||
<div className="inline-flex h-6 min-w-0 items-center gap-1 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark pl-[5px] pr-1.5 shadow-xs">
|
||||
<div className="inline-flex h-6 min-w-0 items-center gap-1 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark pr-1.5 pl-[5px] shadow-xs">
|
||||
<span className={cn(getFieldTypeIconClassName(field.type), 'h-3 w-3 shrink-0 text-text-secondary')} />
|
||||
<span className="truncate text-text-secondary system-xs-medium">{field.label}</span>
|
||||
<span className="truncate system-xs-medium text-text-secondary">{field.label}</span>
|
||||
</div>
|
||||
<span className="shrink-0 text-text-tertiary system-xs-regular">{field.type}</span>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{field.type}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -89,7 +87,7 @@ const ConditionFieldSelect = ({
|
||||
<SelectContent popupClassName="w-[320px]">
|
||||
{groupFieldOptions(fieldOptions).map(([groupName, fields]) => (
|
||||
<SelectGroup key={groupName}>
|
||||
<SelectGroupLabel className="px-3 pb-1 pt-2 text-text-tertiary system-xs-medium-uppercase">{groupName}</SelectGroupLabel>
|
||||
<SelectGroupLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{groupName}</SelectGroupLabel>
|
||||
{fields.map(option => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
@ -116,7 +114,7 @@ const ConditionOperatorSelect = ({
|
||||
return (
|
||||
<Select value={operator} onValueChange={value => value && onChange(value as ComparisonOperator)}>
|
||||
<SelectTrigger className="h-8 w-auto min-w-[88px] gap-1 rounded-md bg-transparent px-1.5 py-0 hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt">
|
||||
<span className="truncate text-text-secondary system-xs-medium">{getOperatorLabel(operator, field?.type, t)}</span>
|
||||
<span className="truncate system-xs-medium text-text-secondary">{getOperatorLabel(operator, field?.type, t)}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[1002]" popupClassName="w-[240px] bg-components-panel-bg-blur backdrop-blur-[10px]">
|
||||
{operators.map(nextOperator => (
|
||||
@ -140,40 +138,6 @@ const FieldValueInput = ({
|
||||
if (!field || !requiresConditionValue(operator))
|
||||
return null
|
||||
|
||||
if (field.type === 'time') {
|
||||
const selectedTime = typeof value === 'string' && value ? dayjs(value) : undefined
|
||||
|
||||
return (
|
||||
<div className="px-2 py-1.5">
|
||||
<DatePicker
|
||||
value={selectedTime}
|
||||
onChange={date => onChange(date ? date.toISOString() : null)}
|
||||
onClear={() => onChange(null)}
|
||||
placeholder={t('conditions.selectTime')}
|
||||
triggerWrapClassName="w-full"
|
||||
popupZIndexClassname="z-[1002]"
|
||||
renderTrigger={({ handleClickTrigger }) => (
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-2 rounded-md px-1 py-1 text-left hover:bg-state-base-hover-alt"
|
||||
onClick={handleClickTrigger}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate system-sm-regular',
|
||||
selectedTime ? 'text-text-secondary' : 'text-components-input-text-placeholder',
|
||||
)}
|
||||
>
|
||||
{selectedTime ? selectedTime.format('MMM D, YYYY h:mm A') : t('conditions.selectTime')}
|
||||
</span>
|
||||
<span className="i-ri-calendar-line h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
return (
|
||||
<div className="px-2 py-1.5">
|
||||
@ -273,7 +237,7 @@ const ConditionGroup = ({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="small" variant="ghost" onClick={() => addConditionItem(resourceType, resourceId, group.id)}>
|
||||
<span aria-hidden="true" className="i-ri-add-line mr-1 h-4 w-4" />
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('conditions.addCondition')}
|
||||
</Button>
|
||||
<Button
|
||||
@ -323,7 +287,7 @@ const ConditionGroup = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pl-1 pt-1">
|
||||
<div className="pt-1 pl-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import type { EvaluationResourceProps } from '../types'
|
||||
import type { EvaluationResourceProps } from '../../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useEvaluationResource, useEvaluationStore } from '../store'
|
||||
import { useEvaluationResource, useEvaluationStore } from '../../store'
|
||||
import { InlineSectionHeader } from '../section-header'
|
||||
import ConditionGroup from './condition-group'
|
||||
import { InlineSectionHeader } from './section-header'
|
||||
|
||||
const ConditionsSection = ({
|
||||
resourceType,
|
||||
@ -24,7 +24,7 @@ const ConditionsSection = ({
|
||||
/>
|
||||
<div className="mt-2 space-y-4">
|
||||
{resource.conditions.length === 0 && (
|
||||
<div className="rounded-xl bg-background-section px-3 py-3 text-text-tertiary system-xs-regular">
|
||||
<div className="rounded-xl bg-background-section px-3 py-3 system-xs-regular text-text-tertiary">
|
||||
{t('conditions.emptyDescription')}
|
||||
</div>
|
||||
)}
|
||||
@ -40,13 +40,13 @@ const ConditionsSection = ({
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex items-center text-text-accent system-sm-medium',
|
||||
'inline-flex items-center system-sm-medium text-text-accent',
|
||||
!canAddCondition && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
|
||||
)}
|
||||
disabled={!canAddCondition}
|
||||
onClick={() => addConditionGroup(resourceType, resourceId)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-add-line mr-1 h-4 w-4" />
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('conditions.addCondition')}
|
||||
</button>
|
||||
</div>
|
||||
@ -102,14 +102,12 @@ const workflowFields: EvaluationFieldOption[] = [
|
||||
{ id: 'app.input.locale', label: 'Locale', group: 'App Input', type: 'enum', options: [{ value: 'en-US', label: 'en-US' }, { value: 'zh-Hans', label: 'zh-Hans' }] },
|
||||
{ id: 'app.output.answer', label: 'Answer', group: 'App Output', type: 'string' },
|
||||
{ id: 'app.output.score', label: 'Score', group: 'App Output', type: 'number' },
|
||||
{ id: 'app.output.published_at', label: 'Publication Date', group: 'App Output', type: 'time' },
|
||||
{ id: 'system.has_context', label: 'Has Context', group: 'System', type: 'boolean' },
|
||||
]
|
||||
|
||||
const pipelineFields: EvaluationFieldOption[] = [
|
||||
{ id: 'dataset.input.document_id', label: 'Document ID', group: 'Dataset', type: 'string' },
|
||||
{ id: 'dataset.input.chunk_count', label: 'Chunk Count', group: 'Dataset', type: 'number' },
|
||||
{ id: 'dataset.input.updated_at', label: 'Updated At', group: 'Dataset', type: 'time' },
|
||||
{ id: 'retrieval.output.hit_rate', label: 'Hit Rate', group: 'Retrieval', type: 'number' },
|
||||
{ id: 'retrieval.output.source', label: 'Source', group: 'Retrieval', type: 'enum', options: [{ value: 'bm25', label: 'BM25' }, { value: 'hybrid', label: 'Hybrid' }] },
|
||||
{ id: 'pipeline.output.published', label: 'Published', group: 'Output', type: 'boolean' },
|
||||
@ -120,7 +118,6 @@ const snippetFields: EvaluationFieldOption[] = [
|
||||
{ id: 'snippet.input.platforms', label: 'Platforms', group: 'Snippet Input', type: 'string' },
|
||||
{ id: 'snippet.output.content', label: 'Generated Content', group: 'Snippet Output', type: 'string' },
|
||||
{ id: 'snippet.output.length', label: 'Output Length', group: 'Snippet Output', type: 'number' },
|
||||
{ id: 'snippet.output.scheduled_at', label: 'Scheduled At', group: 'Snippet Output', type: 'time' },
|
||||
{ id: 'system.requires_review', label: 'Requires Review', group: 'System', type: 'boolean' },
|
||||
]
|
||||
|
||||
@ -128,9 +125,6 @@ export const getComparisonOperators = (fieldType: EvaluationFieldOption['type'])
|
||||
if (fieldType === 'number')
|
||||
return ['is', 'is_not', 'greater_than', 'less_than', 'greater_or_equal', 'less_or_equal', 'is_empty', 'is_not_empty']
|
||||
|
||||
if (fieldType === 'time')
|
||||
return ['is', 'before', 'after', 'is_empty', 'is_not_empty']
|
||||
|
||||
if (fieldType === 'boolean' || fieldType === 'enum')
|
||||
return ['is', 'is_not']
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export type MetricKind = 'builtin' | 'custom-workflow'
|
||||
|
||||
export type BatchTestTab = 'input-fields' | 'history'
|
||||
|
||||
export type FieldType = 'string' | 'number' | 'boolean' | 'enum' | 'time'
|
||||
export type FieldType = 'string' | 'number' | 'boolean' | 'enum'
|
||||
|
||||
export type ComparisonOperator
|
||||
= | 'contains'
|
||||
@ -24,8 +24,6 @@ export type ComparisonOperator
|
||||
| 'less_than'
|
||||
| 'greater_or_equal'
|
||||
| 'less_or_equal'
|
||||
| 'before'
|
||||
| 'after'
|
||||
|
||||
export type JudgeModelOption = {
|
||||
id: string
|
||||
|
||||
@ -53,8 +53,5 @@ export const getFieldTypeIconClassName = (fieldType: EvaluationFieldOption['type
|
||||
if (fieldType === 'enum')
|
||||
return 'i-ri-list-check-2'
|
||||
|
||||
if (fieldType === 'time')
|
||||
return 'i-ri-time-line'
|
||||
|
||||
return 'i-ri-text'
|
||||
}
|
||||
|
||||
@ -27,8 +27,6 @@
|
||||
"conditions.groupLabel": "Group {{index}}",
|
||||
"conditions.logical.and": "AND",
|
||||
"conditions.logical.or": "OR",
|
||||
"conditions.operators.after": "After",
|
||||
"conditions.operators.before": "Before",
|
||||
"conditions.operators.contains": "Contains",
|
||||
"conditions.operators.greater_or_equal": "Greater than or equal",
|
||||
"conditions.operators.greater_than": "Greater than",
|
||||
@ -42,7 +40,6 @@
|
||||
"conditions.removeCondition": "Remove condition",
|
||||
"conditions.removeGroup": "Remove condition group",
|
||||
"conditions.selectFieldFirst": "Select a field first",
|
||||
"conditions.selectTime": "Choose a time...",
|
||||
"conditions.selectValue": "Choose a value",
|
||||
"conditions.title": "Judgment Conditions",
|
||||
"conditions.valuePlaceholder": "Enter a value",
|
||||
|
||||
@ -27,8 +27,6 @@
|
||||
"conditions.groupLabel": "条件组 {{index}}",
|
||||
"conditions.logical.and": "且",
|
||||
"conditions.logical.or": "或",
|
||||
"conditions.operators.after": "晚于",
|
||||
"conditions.operators.before": "早于",
|
||||
"conditions.operators.contains": "包含",
|
||||
"conditions.operators.greater_or_equal": "大于等于",
|
||||
"conditions.operators.greater_than": "大于",
|
||||
@ -42,7 +40,6 @@
|
||||
"conditions.removeCondition": "删除条件",
|
||||
"conditions.removeGroup": "删除条件组",
|
||||
"conditions.selectFieldFirst": "请先选择字段",
|
||||
"conditions.selectTime": "选择时间...",
|
||||
"conditions.selectValue": "选择值",
|
||||
"conditions.title": "判定条件",
|
||||
"conditions.valuePlaceholder": "输入值",
|
||||
@ -53,12 +50,9 @@
|
||||
"metrics.addCustom": "添加自定义指标",
|
||||
"metrics.addNode": "添加节点",
|
||||
"metrics.added": "已添加",
|
||||
"metrics.custom.addMapping": "添加映射",
|
||||
"metrics.custom.description": "选择评测工作流并完成变量映射后即可运行测试。",
|
||||
"metrics.custom.mappingTitle": "变量映射",
|
||||
"metrics.custom.mappingWarning": "请先完成工作流选择和所有变量映射,再运行批量测试。",
|
||||
"metrics.custom.sourcePlaceholder": "源变量",
|
||||
"metrics.custom.targetPlaceholder": "目标变量",
|
||||
"metrics.custom.title": "自定义评测器",
|
||||
"metrics.custom.warningBadge": "待配置",
|
||||
"metrics.custom.workflowLabel": "评测工作流",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user