From 351b909a539185d5af4213eac365a0dd862e5c06 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Tue, 31 Mar 2026 17:00:37 +0800 Subject: [PATCH] feat(web): metric card --- .../components/custom-metric-editor-card.tsx | 106 +++++++------- .../metric-section/__tests__/index.spec.tsx | 2 +- .../components/metric-section/index.tsx | 6 +- .../components/metric-section/metric-card.tsx | 134 +++++++++++++----- .../metric-section-empty-state.tsx | 9 +- 5 files changed, 160 insertions(+), 97 deletions(-) diff --git a/web/app/components/evaluation/components/custom-metric-editor-card.tsx b/web/app/components/evaluation/components/custom-metric-editor-card.tsx index 3bc1db061bf..d9caf5c1370 100644 --- a/web/app/components/evaluation/components/custom-metric-editor-card.tsx +++ b/web/app/components/evaluation/components/custom-metric-editor-card.tsx @@ -2,7 +2,6 @@ import type { CustomMetricMapping, EvaluationMetric, EvaluationResourceProps, EvaluationResourceType } from '../types' import { useTranslation } from 'react-i18next' -import Badge from '@/app/components/base/badge' import Button from '@/app/components/base/button' import { Select, @@ -13,6 +12,7 @@ import { SelectTrigger, SelectValue, } from '@/app/components/base/ui/select' +import { cn } from '@/utils/classnames' import { getEvaluationMockConfig } from '../mock' import { isCustomMetricConfigured, useEvaluationStore } from '../store' import { groupFieldOptions } from '../utils' @@ -40,9 +40,9 @@ function MappingRow({ const config = getEvaluationMockConfig(resourceType) return ( -
+
onUpdate({ targetVariableId: value })}> - + @@ -94,55 +94,59 @@ const CustomMetricEditorCard = ({ return null return ( -
-
-
-
{t('metrics.custom.title')}
-
{t('metrics.custom.description')}
-
- {!isConfigured && {t('metrics.custom.warningBadge')}} -
-
-
-
{t('metrics.custom.workflowLabel')}
- - {selectedWorkflow &&
{selectedWorkflow.description}
} -
-
-
-
{t('metrics.custom.mappingTitle')}
- -
-
- {metric.customConfig.mappings.map(mapping => ( - updateCustomMetricMapping(resourceType, resourceId, metric.id, mapping.id, patch)} - onRemove={() => removeCustomMetricMapping(resourceType, resourceId, metric.id, mapping.id)} - /> - ))} -
- {!isConfigured && ( -
- {t('metrics.custom.mappingWarning')} +
+ + +
+
+
{t('metrics.custom.mappingTitle')}
+
+
+ {metric.customConfig.mappings.map(mapping => ( + updateCustomMetricMapping(resourceType, resourceId, metric.id, mapping.id, patch)} + onRemove={() => removeCustomMetricMapping(resourceType, resourceId, metric.id, mapping.id)} + /> + ))} +
+ {!isConfigured && ( +
+ {t('metrics.custom.mappingWarning')} +
+ )}
) diff --git a/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx b/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx index b112d76f474..b7cb0780d83 100644 --- a/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx +++ b/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx @@ -86,8 +86,8 @@ describe('MetricSection', () => { renderMetricSection() expect(screen.getByText('Custom Evaluator')).toBeInTheDocument() - expect(screen.getByText('evaluation.metrics.custom.title')).toBeInTheDocument() expect(screen.getByText('evaluation.metrics.custom.warningBadge')).toBeInTheDocument() + expect(screen.getByText('evaluation.metrics.custom.workflowPlaceholder')).toBeInTheDocument() expect(screen.getByRole('button', { name: 'evaluation.metrics.custom.addMapping' })).toBeInTheDocument() }) }) diff --git a/web/app/components/evaluation/components/metric-section/index.tsx b/web/app/components/evaluation/components/metric-section/index.tsx index 582fcbfa301..c244e27a8a6 100644 --- a/web/app/components/evaluation/components/metric-section/index.tsx +++ b/web/app/components/evaluation/components/metric-section/index.tsx @@ -22,7 +22,7 @@ const MetricSection = ({ title={t('metrics.title')} tooltip={t('metrics.description')} /> -
+
{!hasMetrics && } {resource.metrics.map(metric => ( ))}
diff --git a/web/app/components/evaluation/components/metric-section/metric-card.tsx b/web/app/components/evaluation/components/metric-section/metric-card.tsx index 70af26a2dce..a1ddc55c32f 100644 --- a/web/app/components/evaluation/components/metric-section/metric-card.tsx +++ b/web/app/components/evaluation/components/metric-section/metric-card.tsx @@ -3,63 +3,125 @@ import type { EvaluationMetric, EvaluationResourceProps } from '../../types' import Badge from '@/app/components/base/badge' import Button from '@/app/components/base/button' -import { useEvaluationStore } from '../../store' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' +import { cn } from '@/utils/classnames' +import { isCustomMetricConfigured, useEvaluationStore } from '../../store' import CustomMetricEditorCard from '../custom-metric-editor-card' +import { getMetricVisual, getNodeVisual, getToneClasses } from '../metric-selector/utils' type MetricCardProps = EvaluationResourceProps & { metric: EvaluationMetric - nodesLabel: string nodesAllLabel: string removeLabel: string + customWarningLabel: string } const MetricCard = ({ resourceType, resourceId, metric, - nodesLabel, nodesAllLabel, removeLabel, + customWarningLabel, }: MetricCardProps) => { + const updateBuiltinMetric = useEvaluationStore(state => state.addBuiltinMetric) const removeMetric = useEvaluationStore(state => state.removeMetric) + const metricVisual = metric.kind === 'custom-workflow' + ? { icon: 'i-ri-equalizer-2-line', tone: 'indigo' as const } + : getMetricVisual(metric.optionId) + const metricToneClasses = getToneClasses(metricVisual.tone) + const isCustomMetricInvalid = metric.kind === 'custom-workflow' && !isCustomMetricConfigured(metric) + const hasSelectedNodes = metric.kind === 'builtin' && !!metric.nodeInfoList?.length return ( -
-
-
-
{metric.label}
-
{metric.description}
-
- {metric.badges.map(badge => ( - {badge} - ))} +
+
+
+
+
+
+
{metric.label}
+ {metric.description && ( + + + + )}
- {metric.kind === 'builtin' && ( -
-
{nodesLabel}
-
- {metric.nodeInfoList?.length - ? metric.nodeInfoList.map(nodeInfo => ( - - {nodeInfo.title} - - )) - : ( - {nodesAllLabel} - )} -
-
- )}
- +
+ {isCustomMetricInvalid && ( + + {customWarningLabel} + + )} + +
+ + {metric.kind === 'builtin' && ( +
+ {metric.nodeInfoList?.length + ? metric.nodeInfoList.map((nodeInfo) => { + const nodeVisual = getNodeVisual(nodeInfo) + const nodeToneClasses = getToneClasses(nodeVisual.tone) + + return ( +
+
+
+ {nodeInfo.title} + +
+ ) + }) + : ( + {nodesAllLabel} + )} +
+ )} + {metric.kind === 'custom-workflow' && ( { return ( -
-
-
-
- {description} -
+
+ {description}
) }