refactor(web): refactor condition group

This commit is contained in:
JzoNg 2026-04-09 19:46:18 +08:00
parent 0438285277
commit 4d1499ef75
9 changed files with 29 additions and 85 deletions

View File

@ -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', () => {

View File

@ -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()
})

View File

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

View File

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

View File

@ -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']

View File

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

View File

@ -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'
}

View File

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

View File

@ -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": "评测工作流",