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 b7cb0780d8..4fb22e7595 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 @@ -48,14 +48,17 @@ describe('MetricSection', () => { // Verify the extracted builtin metric card presentation and removal flow. describe('Builtin Metric Card', () => { it('should render node badges for a builtin metric and remove it when delete is clicked', () => { + // Arrange act(() => { useEvaluationStore.getState().addBuiltinMetric(resourceType, resourceId, 'answer-correctness', [ { node_id: 'node-answer', title: 'Answer Node', type: 'llm' }, ]) }) + // Act renderMetricSection() + // Assert expect(screen.getByText('Answer Correctness')).toBeInTheDocument() expect(screen.getByText('Answer Node')).toBeInTheDocument() @@ -66,14 +69,40 @@ describe('MetricSection', () => { }) it('should render the all-nodes label when a builtin metric has no node selection', () => { + // Arrange act(() => { useEvaluationStore.getState().addBuiltinMetric(resourceType, resourceId, 'answer-correctness', []) }) + // Act renderMetricSection() + // Assert expect(screen.getByText('evaluation.metrics.nodesAll')).toBeInTheDocument() }) + + it('should collapse and expand the node section when the metric header is clicked', () => { + // Arrange + act(() => { + useEvaluationStore.getState().addBuiltinMetric(resourceType, resourceId, 'answer-correctness', [ + { node_id: 'node-answer', title: 'Answer Node', type: 'llm' }, + ]) + }) + + // Act + renderMetricSection() + + const toggleButton = screen.getByRole('button', { name: 'evaluation.metrics.collapseNodes' }) + fireEvent.click(toggleButton) + + // Assert + expect(screen.queryByText('Answer Node')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'evaluation.metrics.expandNodes' })).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'evaluation.metrics.expandNodes' })) + + expect(screen.getByText('Answer Node')).toBeInTheDocument() + }) }) // Verify the extracted custom metric editor card renders inside the metric card. diff --git a/web/app/components/evaluation/components/metric-section/builtin-metric-card.tsx b/web/app/components/evaluation/components/metric-section/builtin-metric-card.tsx new file mode 100644 index 0000000000..ece7c35892 --- /dev/null +++ b/web/app/components/evaluation/components/metric-section/builtin-metric-card.tsx @@ -0,0 +1,101 @@ +'use client' + +import type { EvaluationMetric, EvaluationResourceProps } from '../../types' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import { cn } from '@/utils/classnames' +import { useEvaluationStore } from '../../store' +import { getMetricVisual, getNodeVisual, getToneClasses } from '../metric-selector/utils' + +type BuiltinMetricCardProps = EvaluationResourceProps & { + metric: EvaluationMetric +} + +const BuiltinMetricCard = ({ + resourceType, + resourceId, + metric, +}: BuiltinMetricCardProps) => { + const { t } = useTranslation('evaluation') + const updateBuiltinMetric = useEvaluationStore(state => state.addBuiltinMetric) + const removeMetric = useEvaluationStore(state => state.removeMetric) + const [isExpanded, setIsExpanded] = useState(true) + const metricVisual = getMetricVisual(metric.optionId) + const metricToneClasses = getToneClasses(metricVisual.tone) + + return ( +
+
+ + + +
+ + {isExpanded && ( +
+ {metric.nodeInfoList?.length + ? metric.nodeInfoList.map((nodeInfo) => { + const nodeVisual = getNodeVisual(nodeInfo) + const nodeToneClasses = getToneClasses(nodeVisual.tone) + + return ( +
+
+
+ {nodeInfo.title} + +
+ ) + }) + : ( + {t('metrics.nodesAll')} + )} +
+ )} +
+ ) +} + +export default BuiltinMetricCard diff --git a/web/app/components/evaluation/components/metric-section/custom-metric-card.tsx b/web/app/components/evaluation/components/metric-section/custom-metric-card.tsx new file mode 100644 index 0000000000..e327ffbf63 --- /dev/null +++ b/web/app/components/evaluation/components/metric-section/custom-metric-card.tsx @@ -0,0 +1,63 @@ +'use client' + +import type { EvaluationMetric, EvaluationResourceProps } from '../../types' +import { useTranslation } from 'react-i18next' +import Badge from '@/app/components/base/badge' +import Button from '@/app/components/base/button' +import { cn } from '@/utils/classnames' +import { isCustomMetricConfigured, useEvaluationStore } from '../../store' +import CustomMetricEditorCard from '../custom-metric-editor-card' +import { getToneClasses } from '../metric-selector/utils' + +type CustomMetricCardProps = EvaluationResourceProps & { + metric: EvaluationMetric +} + +const CustomMetricCard = ({ + resourceType, + resourceId, + metric, +}: CustomMetricCardProps) => { + const { t } = useTranslation('evaluation') + const removeMetric = useEvaluationStore(state => state.removeMetric) + const isCustomMetricInvalid = !isCustomMetricConfigured(metric) + const metricToneClasses = getToneClasses('indigo') + + return ( +
+
+
+
+
+
{metric.label}
+
+ +
+ {isCustomMetricInvalid && ( + + {t('metrics.custom.warningBadge')} + + )} + +
+
+ + +
+ ) +} + +export default CustomMetricCard diff --git a/web/app/components/evaluation/components/metric-section/index.tsx b/web/app/components/evaluation/components/metric-section/index.tsx index f9f8ccc33b..70a9606ee5 100644 --- a/web/app/components/evaluation/components/metric-section/index.tsx +++ b/web/app/components/evaluation/components/metric-section/index.tsx @@ -30,9 +30,6 @@ const MetricSection = ({ resourceType={resourceType} resourceId={resourceId} metric={metric} - nodesAllLabel={t('metrics.nodesAll')} - removeLabel={t('metrics.remove')} - customWarningLabel={t('metrics.custom.warningBadge')} /> ))} { - 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 + if (metric.kind === 'custom-workflow') { + return ( + + ) + } return ( -
-
-
-
-
-
-
{metric.label}
- {metric.description && ( - - - - )} -
-
-
- {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' && ( - - )} -
+ ) } diff --git a/web/i18n/en-US/evaluation.json b/web/i18n/en-US/evaluation.json index 9fb4198bb0..a691f99fa2 100644 --- a/web/i18n/en-US/evaluation.json +++ b/web/i18n/en-US/evaluation.json @@ -52,6 +52,7 @@ "metrics.add": "Add Metric", "metrics.addCustom": "Add Custom Metrics", "metrics.added": "Added", + "metrics.collapseNodes": "Collapse nodes", "metrics.custom.addMapping": "Add Mapping", "metrics.custom.description": "Select an evaluation workflow and map your variables before running tests.", "metrics.custom.footerDescription": "Connect your published evaluation workflows", @@ -65,6 +66,7 @@ "metrics.custom.workflowLabel": "Evaluation Workflow", "metrics.custom.workflowPlaceholder": "Select a workflow", "metrics.description": "Choose from built-in metrics like Groundedness and Correctness to evaluate your workflow outputs.", + "metrics.expandNodes": "Expand nodes", "metrics.groups.operations": "Operations", "metrics.groups.other": "Other", "metrics.groups.quality": "Quality",