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 (
+
+
+
+
+
+ {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 && (
-
-
-
-
- )}
- />
-
- {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",