fix(web): style of conditions

This commit is contained in:
JzoNg 2026-04-29 17:15:32 +08:00
parent 9af2c1252c
commit dacc7fc740
7 changed files with 151 additions and 140 deletions

View File

@ -366,7 +366,7 @@ describe('Evaluation', () => {
render(<ConditionsSection resourceType={resourceType} resourceId={resourceId} />)
fireEvent.click(screen.getByRole('combobox', { name: 'evaluation.conditions.addCondition' }))
fireEvent.click(screen.getByRole('button', { name: 'evaluation.conditions.addCondition' }))
expect(screen.getByText('Faithfulness')).toBeInTheDocument()
expect(screen.getByText('Review Workflow')).toBeInTheDocument()
@ -375,7 +375,7 @@ describe('Evaluation', () => {
expect(screen.getByText('evaluation.conditions.valueTypes.number')).toBeInTheDocument()
expect(screen.getByText('evaluation.conditions.valueTypes.string')).toBeInTheDocument()
fireEvent.click(screen.getByRole('option', { name: /reason/i }))
fireEvent.click(screen.getByRole('menuitem', { name: /reason/i }))
const condition = useEvaluationStore.getState().resources['apps:app-conditions-dropdown'].judgmentConfig.conditions[0]

View File

@ -1,15 +1,12 @@
'use client'
import type { ConditionMetricOptionGroup, EvaluationResourceProps } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Button } from '@langgenius/dify-ui/button'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
} from '@langgenius/dify-ui/select'
Popover,
PopoverContent,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useEvaluationStore } from '../../store'
@ -28,47 +25,59 @@ const AddConditionSelect = ({
}: AddConditionSelectProps) => {
const { t } = useTranslation('evaluation')
const addCondition = useEvaluationStore(state => state.addCondition)
const [selectKey, setSelectKey] = useState(0)
const [open, setOpen] = useState(false)
const handleOpenChange = (nextOpen: boolean) => {
if (disabled)
return
setOpen(nextOpen)
}
return (
<Select key={selectKey}>
<SelectTrigger
aria-label={t('conditions.addCondition')}
className={cn(
'inline-flex w-auto min-w-0 border-none bg-transparent px-0 py-0 text-text-accent shadow-none hover:bg-transparent focus-visible:bg-transparent',
disabled && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={(
<Button
variant="ghost-accent"
aria-label={t('conditions.addCondition')}
disabled={disabled}
>
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
{t('conditions.addCondition')}
</Button>
)}
disabled={disabled}
/>
<PopoverContent
placement="bottom-start"
popupClassName="w-[320px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border p-0 shadow-[0px_12px_16px_-4px_rgba(9,9,11,0.08),0px_4px_6px_-2px_rgba(9,9,11,0.03)]"
>
<span aria-hidden="true" className="i-ri-add-line h-4 w-4" />
{t('conditions.addCondition')}
</SelectTrigger>
<SelectContent placement="bottom-start" popupClassName="w-[320px]">
{metricOptionGroups.map(group => (
<SelectGroup key={group.label}>
<SelectLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</SelectLabel>
{group.options.map(option => (
<SelectItem
key={option.id}
value={option.id}
className="h-auto gap-0 px-3 py-2"
onClick={() => {
addCondition(resourceType, resourceId, option.variableSelector)
setSelectKey(current => current + 1)
}}
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<span className="truncate system-sm-medium text-text-secondary">{option.itemLabel}</span>
<div className="max-h-[360px] overflow-y-auto bg-components-panel-bg p-1" role="menu">
{metricOptionGroups.map(group => (
<div key={group.label} role="group" aria-label={group.label}>
<div className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</div>
{group.options.map(option => (
<button
key={option.id}
type="button"
role="menuitem"
className="flex h-auto w-full items-center gap-3 overflow-hidden rounded-lg px-3 py-2 text-left hover:bg-components-panel-on-panel-item-bg-hover"
onClick={() => {
addCondition(resourceType, resourceId, option.variableSelector)
setOpen(false)
}}
>
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">{option.itemLabel}</span>
<span className="ml-auto shrink-0 system-xs-medium text-text-tertiary">
{t(getConditionMetricValueTypeTranslationKey(option.valueType))}
</span>
</div>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</button>
))}
</div>
))}
</div>
</PopoverContent>
</Popover>
)
}

View File

@ -6,7 +6,6 @@ import type {
EvaluationResourceProps,
JudgmentConditionItem,
} from '../../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
Select,
@ -19,7 +18,9 @@ import {
} from '@langgenius/dify-ui/select'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input'
import BlockIcon from '@/app/components/workflow/block-icon'
import { getAllowedOperators, requiresConditionValue, useEvaluationResource, useEvaluationStore } from '../../store'
import {
buildConditionMetricOptions,
@ -29,6 +30,7 @@ import {
isSelectorEqual,
serializeVariableSelector,
} from '../../utils'
import { getEvaluationNodeBlockType } from '../metric-selector/utils'
type ConditionMetricLabelProps = {
metric?: ConditionMetricOption
@ -56,14 +58,8 @@ type ConditionValueInputProps = {
type ConditionGroupProps = EvaluationResourceProps
const getMetricValueTypeIconClassName = (valueType: ConditionMetricOption['valueType']) => {
if (valueType === 'number')
return 'i-ri-hashtag'
if (valueType === 'boolean')
return 'i-ri-checkbox-circle-line'
return 'i-ri-bar-chart-box-line'
const getMetricVariableLabel = (variableName: string) => {
return variableName.replaceAll('-', '_')
}
const ConditionMetricLabel = ({
@ -73,13 +69,28 @@ const ConditionMetricLabel = ({
if (!metric)
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 pr-1.5 pl-[5px] shadow-xs">
<span className={cn(getMetricValueTypeIconClassName(metric.valueType), 'h-3 w-3 shrink-0 text-text-secondary')} />
<span className="truncate system-xs-medium text-text-secondary">{metric.itemLabel}</span>
if (metric.kind === 'builtin' && metric.nodeInfo) {
return (
<div className="flex min-w-0 items-center px-1">
<div className="inline-flex h-6 min-w-0 items-center gap-0.5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark py-1 pr-1.5 pl-[5px] shadow-xs">
<span className="truncate system-xs-medium text-text-secondary">{getMetricVariableLabel(metric.variableSelector[1])}</span>
<span className="system-xs-regular text-divider-deep">/</span>
<span className="flex min-w-0 shrink-0 items-center gap-0.5">
<BlockIcon type={getEvaluationNodeBlockType(metric.nodeInfo)} size="xs" className="size-3 rounded-[5px]" />
<span className="max-w-[96px] truncate system-xs-medium text-text-secondary">{metric.itemLabel}</span>
</span>
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.valueType}</span>
</div>
</div>
)
}
return (
<div className="flex min-w-0 items-center px-1">
<div className="inline-flex h-6 min-w-0 items-center gap-0.5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark py-1 pr-1.5 pl-[5px] shadow-xs">
<span className="truncate system-xs-medium text-text-secondary">{metric.itemLabel}</span>
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.valueType}</span>
</div>
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.groupLabel}</span>
</div>
)
}
@ -114,7 +125,6 @@ const ConditionMetricSelect = ({
{group.options.map(option => (
<SelectItem key={option.id} value={serializeVariableSelector(option.variableSelector)}>
<div className="flex min-w-0 flex-1 items-center gap-2">
<span className={cn(getMetricValueTypeIconClassName(option.valueType), 'h-3.5 w-3.5 shrink-0 text-text-tertiary')} />
<span className="truncate">{option.itemLabel}</span>
<span className="ml-auto shrink-0 system-xs-medium text-text-quaternary">
{t(getConditionMetricValueTypeTranslationKey(option.valueType))}
@ -141,7 +151,7 @@ const ConditionOperatorSelect = ({
<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 system-xs-medium text-text-secondary">{getComparisonOperatorLabel(operator, t)}</span>
</SelectTrigger>
<SelectContent className="z-[1002]" popupClassName="w-[240px] bg-components-panel-bg-blur backdrop-blur-[10px]">
<SelectContent className="z-1002" popupClassName="w-[240px] bg-components-panel-bg-blur backdrop-blur-[10px]">
{operators.map(nextOperator => (
<SelectItem key={nextOperator} value={nextOperator}>
{getComparisonOperatorLabel(nextOperator, t)}
@ -212,88 +222,87 @@ const ConditionGroup = ({
const { t } = useTranslation('evaluation')
const resource = useEvaluationResource(resourceType, resourceId)
const metricOptions = useMemo(() => buildConditionMetricOptions(resource.metrics), [resource.metrics])
const logicalOperator = resource.judgmentConfig.logicalOperator
const logicalLabels = {
and: t('conditions.logical.and'),
or: t('conditions.logical.or'),
}
const hasMultipleConditions = resource.judgmentConfig.conditions.length > 1
const setConditionLogicalOperator = useEvaluationStore(state => state.setConditionLogicalOperator)
const removeCondition = useEvaluationStore(state => state.removeCondition)
const updateConditionMetric = useEvaluationStore(state => state.updateConditionMetric)
const updateConditionOperator = useEvaluationStore(state => state.updateConditionOperator)
const updateConditionValue = useEvaluationStore(state => state.updateConditionValue)
const toggleLogicalOperator = () => {
setConditionLogicalOperator(resourceType, resourceId, logicalOperator === 'and' ? 'or' : 'and')
}
return (
<div className="rounded-2xl border border-divider-subtle bg-components-card-bg p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div className="flex rounded-lg border border-divider-subtle bg-background-default-subtle p-1">
{(['and', 'or'] as const).map(operator => (
<button
key={operator}
type="button"
className={cn(
'rounded-md px-3 py-1.5 system-xs-medium-uppercase',
resource.judgmentConfig.logicalOperator === operator
? 'bg-components-card-bg text-text-primary shadow-xs'
: 'text-text-tertiary',
)}
onClick={() => setConditionLogicalOperator(resourceType, resourceId, operator)}
>
{logicalLabels[operator]}
</button>
))}
<div className={cn('relative', hasMultipleConditions && 'pl-[48px]')}>
{hasMultipleConditions && (
<div className="absolute top-0 bottom-0 left-0 w-[48px]">
<div className="absolute top-4 bottom-4 left-[34px] w-2.5 rounded-l-[8px] border border-r-0 border-divider-deep" />
<div className="absolute top-1/2 right-0 h-[29px] w-4 -translate-y-1/2 bg-components-card-bg" />
<button
type="button"
aria-label={logicalLabels[logicalOperator]}
className="absolute top-1/2 right-1 flex h-[21px] -translate-y-1/2 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-1 text-[10px] font-semibold text-text-accent-secondary shadow-xs select-none"
onClick={toggleLogicalOperator}
>
{logicalLabels[logicalOperator]}
<span aria-hidden="true" className="ml-0.5 i-ri-loop-left-line h-3 w-3" />
</button>
</div>
</div>
</div>
)}
<div className="space-y-3">
{resource.judgmentConfig.conditions.map((condition) => {
const metric = metricOptions.find(option => isSelectorEqual(option.variableSelector, condition.variableSelector))
const allowedOperators = getAllowedOperators(resource.metrics, condition.variableSelector)
const showValue = !!metric && requiresConditionValue(condition.comparisonOperator)
<div className="space-y-3">
{resource.judgmentConfig.conditions.map((condition) => {
const metric = metricOptions.find(option => isSelectorEqual(option.variableSelector, condition.variableSelector))
const allowedOperators = getAllowedOperators(resource.metrics, condition.variableSelector)
const showValue = !!metric && requiresConditionValue(condition.comparisonOperator)
return (
<div key={condition.id} className="flex items-start overflow-hidden rounded-lg">
<div className="min-w-0 flex-1 rounded-lg bg-components-input-bg-normal">
<div className="flex items-center gap-0 pr-1">
<div className="min-w-0 flex-1 py-1">
<ConditionMetricSelect
metric={metric}
metricOptions={metricOptions}
placeholder={t('conditions.fieldPlaceholder')}
onChange={value => updateConditionMetric(resourceType, resourceId, condition.id, value)}
return (
<div key={condition.id} className="flex items-start overflow-hidden rounded-lg">
<div className="min-w-0 flex-1 rounded-lg bg-components-input-bg-normal">
<div className="flex items-center gap-0 pr-1">
<div className="min-w-0 flex-1 py-1">
<ConditionMetricSelect
metric={metric}
metricOptions={metricOptions}
placeholder={t('conditions.fieldPlaceholder')}
onChange={value => updateConditionMetric(resourceType, resourceId, condition.id, value)}
/>
</div>
<div className="h-3 w-px bg-divider-regular" />
<ConditionOperatorSelect
operator={condition.comparisonOperator}
operators={allowedOperators}
onChange={value => updateConditionOperator(resourceType, resourceId, condition.id, value)}
/>
</div>
<div className="h-3 w-px bg-divider-regular" />
<ConditionOperatorSelect
operator={condition.comparisonOperator}
operators={allowedOperators}
onChange={value => updateConditionOperator(resourceType, resourceId, condition.id, value)}
/>
{showValue && (
<div className="border-t border-divider-subtle">
<ConditionValueInput
metric={metric}
condition={condition}
onChange={value => updateConditionValue(resourceType, resourceId, condition.id, value)}
/>
</div>
)}
</div>
<div className="pt-1 pl-1">
<ActionButton
aria-label={t('conditions.removeCondition')}
onClick={() => removeCondition(resourceType, resourceId, condition.id)}
>
<span aria-hidden="true" className="i-ri-delete-bin-line h-4 w-4" />
</ActionButton>
</div>
{showValue && (
<div className="border-t border-divider-subtle">
<ConditionValueInput
metric={metric}
condition={condition}
onChange={value => updateConditionValue(resourceType, resourceId, condition.id, value)}
/>
</div>
)}
</div>
<div className="pt-1 pl-1">
<Button
size="small"
variant="ghost"
aria-label={t('conditions.removeCondition')}
onClick={() => removeCondition(resourceType, resourceId, condition.id)}
>
<span aria-hidden="true" className="i-ri-close-line h-4 w-4" />
</Button>
</div>
</div>
)
})}
)
})}
</div>
</div>
</div>
)

View File

@ -3,7 +3,6 @@
import type { ChangeEvent } from 'react'
import type { MetricSelectorProps } from './types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
PopoverContent,
@ -22,7 +21,6 @@ const MetricSelector = ({
resourceType,
resourceId,
triggerClassName,
triggerStyle = 'button',
}: MetricSelectorProps) => {
const { t } = useTranslation('evaluation')
const resource = useEvaluationResource(resourceType, resourceId)
@ -63,19 +61,10 @@ const MetricSelector = ({
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={(
triggerStyle === 'text'
? (
<button type="button" className={cn('inline-flex items-center system-sm-medium text-text-accent', triggerClassName)}>
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
{t('metrics.add')}
</button>
)
: (
<Button variant="ghost-accent" className={triggerClassName}>
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
{t('metrics.add')}
</Button>
)
<Button variant="ghost-accent" className={triggerClassName}>
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
{t('metrics.add')}
</Button>
)}
/>
<PopoverContent popupClassName="w-[360px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border p-0 shadow-[0px_12px_16px_-4px_rgba(9,9,11,0.08),0px_4px_6px_-2px_rgba(9,9,11,0.03)]">

View File

@ -3,7 +3,6 @@ import type { NodeInfo } from '@/types/evaluation'
export type MetricSelectorProps = NonPipelineEvaluationResourceProps & {
triggerClassName?: string
triggerStyle?: 'button' | 'text'
}
export type MetricVisualTone = 'indigo' | 'green'

View File

@ -89,10 +89,12 @@ export type JudgmentConfig = {
export type ConditionMetricOption = {
id: string
kind: MetricKind
groupLabel: string
itemLabel: string
valueType: ConditionMetricValueType
variableSelector: [string, string]
nodeInfo?: NodeInfo
}
export type ConditionMetricOptionGroup = {

View File

@ -70,10 +70,12 @@ export const buildConditionMetricOptions = (metrics: EvaluationMetric[]): Condit
return (metric.nodeInfoList ?? []).map((nodeInfo) => {
return {
id: `${nodeInfo.node_id}:${metric.optionId}`,
kind: metric.kind,
groupLabel: metric.label,
itemLabel: nodeInfo.title || nodeInfo.node_id,
valueType: metric.valueType,
variableSelector: [nodeInfo.node_id, metric.optionId] as [string, string],
nodeInfo,
}
})
}
@ -86,6 +88,7 @@ export const buildConditionMetricOptions = (metrics: EvaluationMetric[]): Condit
return customConfig.outputs.map((output) => {
return {
id: `${customConfig.workflowId}:${output.id}`,
kind: metric.kind,
groupLabel: customConfig.workflowName ?? metric.label,
itemLabel: output.id,
valueType: getMetricValueType(output.valueType),