fix(web): style of default metric

This commit is contained in:
JzoNg 2026-04-29 14:31:23 +08:00
parent 2607eb8d32
commit b420298398
7 changed files with 66 additions and 71 deletions

View File

@ -9,7 +9,8 @@ import {
} from '@langgenius/dify-ui/popover'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getNodeVisual, getToneClasses } from '@/app/components/evaluation/components/metric-selector/utils'
import { getEvaluationNodeBlockType } from '@/app/components/evaluation/components/metric-selector/utils'
import BlockIcon from '@/app/components/workflow/block-icon'
type EvaluationCellProps = {
evaluation: EvaluationLogItem[]
@ -60,37 +61,34 @@ const EvaluationCell = ({
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)]"
>
<div data-testid="workflow-log-evaluation-popover" className="max-h-[320px] overflow-y-auto bg-components-panel-bg">
{evaluation.map((item, index) => {
const nodeVisual = item.nodeInfo ? getNodeVisual(item.nodeInfo) : null
const nodeToneClasses = nodeVisual ? getToneClasses(nodeVisual.tone) : null
return (
<div
key={`${item.name}-${index}`}
className={cn(
'grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-4 py-3',
index !== evaluation.length - 1 && 'border-b border-divider-subtle',
{evaluation.map((item, index) => (
<div
key={item.nodeInfo ? `${item.name}-${item.nodeInfo.node_id}` : item.name}
className={cn(
'grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-4 py-3',
index !== evaluation.length - 1 && 'border-b border-divider-subtle',
)}
>
<div className="min-w-0">
<div className="truncate system-sm-medium text-text-secondary">{item.name}</div>
{item.nodeInfo && (
<div className="mt-1 flex min-w-0 items-center gap-1.5">
<BlockIcon
type={getEvaluationNodeBlockType(item.nodeInfo)}
size="xs"
className="h-[18px] w-[18px] shrink-0"
/>
<span className="truncate system-xs-regular text-text-tertiary">
{item.nodeInfo.title}
</span>
</div>
)}
>
<div className="min-w-0">
<div className="truncate system-sm-medium text-text-secondary">{item.name}</div>
{item.nodeInfo && nodeVisual && nodeToneClasses && (
<div className="mt-1 flex min-w-0 items-center gap-1.5">
<div className={cn('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border-[0.45px] border-divider-subtle shadow-xs shadow-shadow-shadow-3', nodeToneClasses.solid)}>
<span aria-hidden="true" className={cn(nodeVisual.icon, 'h-3.5 w-3.5')} />
</div>
<span className="truncate system-xs-regular text-text-tertiary">
{item.nodeInfo.title}
</span>
</div>
)}
</div>
<div className="max-w-[120px] text-right system-sm-regular wrap-break-word text-text-secondary">
{formatEvaluationValue(item.value)}
</div>
</div>
)
})}
<div className="max-w-[120px] text-right system-sm-regular wrap-break-word text-text-secondary">
{formatEvaluationValue(item.value)}
</div>
</div>
))}
</div>
</PopoverContent>
</Popover>

View File

@ -12,8 +12,9 @@ import {
} from '@langgenius/dify-ui/dropdown-menu'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import BlockIcon from '@/app/components/workflow/block-icon'
import { useEvaluationStore } from '../../store'
import { dedupeNodeInfoList, getMetricVisual, getNodeVisual, getToneClasses } from '../metric-selector/utils'
import { dedupeNodeInfoList, getEvaluationNodeBlockType, getMetricVisual, getToneClasses } from '../metric-selector/utils'
type BuiltinMetricCardProps = EvaluationResourceProps & {
metric: EvaluationMetric
@ -41,7 +42,7 @@ const BuiltinMetricCard = ({
return (
<div className="group overflow-hidden rounded-xl border border-components-panel-border hover:bg-background-section">
<div className="flex items-center justify-between gap-3 px-3 pt-3 pb-1">
<div className={cn('flex items-center justify-between gap-3 px-3 pt-3', isExpanded ? 'pb-1' : 'pb-3')}>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-2 px-1 text-left"
@ -76,17 +77,16 @@ const BuiltinMetricCard = ({
<div className="flex flex-wrap gap-1 px-3 pt-1 pb-3">
{selectedNodeInfoList.length
? selectedNodeInfoList.map((nodeInfo) => {
const nodeVisual = getNodeVisual(nodeInfo)
const nodeToneClasses = getToneClasses(nodeVisual.tone)
return (
<div
key={nodeInfo.node_id}
className="inline-flex min-w-[18px] items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark p-1.5 shadow-xs"
>
<div className={cn('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border-[0.45px] border-divider-subtle shadow-xs shadow-shadow-shadow-3', nodeToneClasses.solid)}>
<span aria-hidden="true" className={cn(nodeVisual.icon, 'h-3.5 w-3.5')} />
</div>
<BlockIcon
type={getEvaluationNodeBlockType(nodeInfo)}
size="xs"
className="h-[18px] w-[18px] shrink-0"
/>
<span className="px-1 system-xs-regular text-text-primary">{nodeInfo.title}</span>
<button
type="button"
@ -99,7 +99,7 @@ const BuiltinMetricCard = ({
selectedNodeInfoList.filter(item => item.node_id !== nodeInfo.node_id),
)}
>
<span aria-hidden="true" className="i-ri-close-line h-3.5 w-3.5" />
<span aria-hidden="true" className="i-custom-vender-solid-general-x-circle h-3.5 w-3.5" />
</button>
</div>
)
@ -126,9 +126,6 @@ const BuiltinMetricCard = ({
popupClassName="w-[252px] rounded-md border-[0.5px] border-components-panel-border py-1 shadow-[0px_12px_16px_-4px_rgba(9,9,11,0.08),0px_4px_6px_-2px_rgba(9,9,11,0.03)]"
>
{selectableNodeInfoList.map((nodeInfo) => {
const nodeVisual = getNodeVisual(nodeInfo)
const nodeToneClasses = getToneClasses(nodeVisual.tone)
return (
<DropdownMenuItem
key={nodeInfo.node_id}
@ -141,9 +138,11 @@ const BuiltinMetricCard = ({
)}
>
<div className="flex min-w-0 flex-1 items-center gap-2.5 pr-1">
<div className={cn('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border-[0.45px] border-divider-subtle shadow-xs shadow-shadow-shadow-3', nodeToneClasses.solid)}>
<span aria-hidden="true" className={cn(nodeVisual.icon, 'h-3.5 w-3.5')} />
</div>
<BlockIcon
type={getEvaluationNodeBlockType(nodeInfo)}
size="xs"
className="h-[18px] w-[18px] shrink-0"
/>
<span className="truncate system-sm-medium text-text-secondary">{nodeInfo.title}</span>
</div>
</DropdownMenuItem>

View File

@ -5,9 +5,9 @@ type SelectorEmptyStateProps = {
const EmptySearchStateIcon = () => {
return (
<div className="relative h-8 w-8 text-text-quaternary">
<span aria-hidden="true" className="i-ri-search-line absolute bottom-0 right-0 h-6 w-6" />
<span aria-hidden="true" className="absolute left-0 top-[9px] h-[2px] w-[7px] rounded-full bg-current opacity-80" />
<span aria-hidden="true" className="absolute left-0 top-[16px] h-[2px] w-[4px] rounded-full bg-current opacity-80" />
<span aria-hidden="true" className="absolute right-0 bottom-0 i-ri-search-line h-6 w-6" />
<span aria-hidden="true" className="absolute top-[9px] left-0 h-[2px] w-[7px] rounded-full bg-current opacity-80" />
<span aria-hidden="true" className="absolute top-[16px] left-0 h-[2px] w-[4px] rounded-full bg-current opacity-80" />
</div>
)
}
@ -18,7 +18,7 @@ const SelectorEmptyState = ({
return (
<div className="flex h-full min-h-[524px] flex-col items-center justify-center gap-2 px-4 pb-20 text-center">
<EmptySearchStateIcon />
<div className="text-text-secondary system-sm-regular">{message}</div>
<div className="system-sm-regular text-text-secondary">{message}</div>
</div>
)
}

View File

@ -2,7 +2,8 @@ import type { TFunction } from 'i18next'
import type { EvaluationMetric } from '../../types'
import type { MetricSelectorSection } from './types'
import { cn } from '@langgenius/dify-ui/cn'
import { getMetricVisual, getNodeVisual, getToneClasses } from './utils'
import BlockIcon from '@/app/components/workflow/block-icon'
import { getEvaluationNodeBlockType, getMetricVisual, getToneClasses } from './utils'
type SelectorMetricSectionProps = {
section: MetricSelectorSection
@ -56,7 +57,7 @@ const SelectorMetricSection = ({
<span aria-hidden="true" className={cn(metricVisual.icon, 'h-3.5 w-3.5')} />
</div>
<div className="flex items-center gap-1">
<span className="system-xs-medium-uppercase truncate text-text-secondary">{metric.label}</span>
<span className="truncate system-xs-medium-uppercase text-text-secondary">{metric.label}</span>
<span
aria-hidden="true"
className={cn('i-ri-arrow-down-s-line h-4 w-4 text-text-quaternary transition-transform', !isExpanded && '-rotate-90')}
@ -72,13 +73,11 @@ const SelectorMetricSection = ({
{isExpanded && (
<div className="px-1 py-1">
{hasNoNodeInfo && (
<div className="system-sm-regular px-3 pt-0.5 pb-2 text-text-tertiary">
<div className="px-3 pt-0.5 pb-2 system-sm-regular text-text-tertiary">
{t('metrics.noNodesInWorkflow')}
</div>
)}
{shownNodes.map((nodeInfo) => {
const nodeVisual = getNodeVisual(nodeInfo)
const nodeToneClasses = getToneClasses(nodeVisual.tone)
const isAdded = addedMetric
? addedMetric.nodeInfoList?.length
? selectedNodeIds.has(nodeInfo.node_id)
@ -91,21 +90,23 @@ const SelectorMetricSection = ({
data-testid={`evaluation-metric-node-${metric.id}-${nodeInfo.node_id}`}
type="button"
className={cn(
'flex w-full items-center gap-1 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-state-base-hover-alt',
'flex w-full items-center gap-1 rounded-md px-3 py-1.5 text-left transition-colors hover:bg-state-base-hover-alt',
isAdded && 'opacity-50',
)}
onClick={() => onToggleNodeSelection(metric.id, nodeInfo)}
>
<div className="flex min-w-0 flex-1 items-center gap-2.5 pr-1">
<div className={cn('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border-[0.45px] border-divider-subtle shadow-xs shadow-shadow-shadow-3', nodeToneClasses.solid)}>
<span aria-hidden="true" className={cn(nodeVisual.icon, 'h-3.5 w-3.5')} />
</div>
<BlockIcon
type={getEvaluationNodeBlockType(nodeInfo)}
size="xs"
className="h-[18px] w-[18px] shrink-0"
/>
<span className="truncate text-[13px] leading-4 font-medium text-text-secondary">
{nodeInfo.title}
</span>
</div>
{isAdded && (
<span className="system-xs-regular shrink-0 px-1 text-text-quaternary">{t('metrics.added')}</span>
<span className="shrink-0 px-1 system-xs-regular text-text-quaternary">{t('metrics.added')}</span>
)}
</button>
)
@ -120,7 +121,7 @@ const SelectorMetricSection = ({
<div className="flex items-center px-1 text-text-tertiary">
<span aria-hidden="true" className={cn(isShowingAllNodes ? 'i-ri-subtract-line' : 'i-ri-more-line', 'h-4 w-4')} />
</div>
<span className="system-xs-regular truncate text-text-tertiary">
<span className="truncate system-xs-regular text-text-tertiary">
{isShowingAllNodes ? t('metrics.showLess') : t('metrics.showMore')}
</span>
</div>

View File

@ -1,6 +1,7 @@
import type { ConditionMetricValueType, MetricOption } from '../../types'
import type { MetricVisualTone } from './types'
import type { EvaluationDefaultMetric, NodeInfo } from '@/types/evaluation'
import { BlockEnum } from '@/app/components/workflow/types'
import { getDefaultMetricDescription } from '../../default-metric-descriptions'
const defaultConditionMetricValueType: ConditionMetricValueType = 'number'
@ -70,17 +71,13 @@ export const getMetricVisual = (metricId: string): { icon: string, tone: MetricV
return { icon: 'i-ri-checkbox-circle-line', tone: 'indigo' }
}
export const getNodeVisual = (nodeInfo: NodeInfo): { icon: string, tone: MetricVisualTone } => {
const normalizedType = nodeInfo.type.toLowerCase()
const normalizedTitle = nodeInfo.title.toLowerCase()
const workflowBlockTypeSet = new Set<string>(Object.values(BlockEnum))
if (normalizedType.includes('retriev') || normalizedTitle.includes('retriev') || normalizedTitle.includes('knowledge'))
return { icon: 'i-ri-book-open-line', tone: 'green' }
export const getEvaluationNodeBlockType = (nodeInfo: Pick<NodeInfo, 'type'>): BlockEnum => {
if (workflowBlockTypeSet.has(nodeInfo.type))
return nodeInfo.type as BlockEnum
if (normalizedType.includes('agent') || normalizedTitle.includes('agent'))
return { icon: 'i-ri-user-star-line', tone: 'indigo' }
return { icon: 'i-ri-ai-generate-2', tone: 'indigo' }
return BlockEnum.LLM
}
export const getToneClasses = (tone: MetricVisualTone) => {

View File

@ -110,7 +110,7 @@
"metrics.groups.operations": "Operations",
"metrics.groups.other": "Other",
"metrics.groups.quality": "Quality",
"metrics.noNodesInWorkflow": "No LLM nodes in this workflow",
"metrics.noNodesInWorkflow": "No selectable nodes",
"metrics.noResults": "No metrics or nodes were found",
"metrics.nodesAll": "All nodes",
"metrics.nodesLabel": "Node Scope",

View File

@ -104,7 +104,7 @@
"metrics.groups.operations": "运行",
"metrics.groups.other": "其他",
"metrics.groups.quality": "质量",
"metrics.noNodesInWorkflow": "当前工作流中没有 LLM 节点",
"metrics.noNodesInWorkflow": "没有可选节点",
"metrics.noResults": "没有匹配的指标。",
"metrics.nodesAll": "全部节点",
"metrics.nodesLabel": "节点范围",