- {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)
+
+ {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 (
-
-
-
-
-
updateConditionMetric(resourceType, resourceId, condition.id, value)}
+ return (
+
+
+
+
+ updateConditionMetric(resourceType, resourceId, condition.id, value)}
+ />
+
+
+
updateConditionOperator(resourceType, resourceId, condition.id, value)}
/>
-
-
updateConditionOperator(resourceType, resourceId, condition.id, value)}
- />
+ {showValue && (
+
+ updateConditionValue(resourceType, resourceId, condition.id, value)}
+ />
+
+ )}
+
+
+
removeCondition(resourceType, resourceId, condition.id)}
+ >
+
+
- {showValue && (
-
- updateConditionValue(resourceType, resourceId, condition.id, value)}
- />
-
- )}
-
-
-
-
- )
- })}
+ )
+ })}
+
)
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 6b240966bf..4cdf58a5dc 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
@@ -131,6 +131,23 @@ describe('MetricSection', () => {
expect(screen.getByText('Answer Node')).toBeInTheDocument()
})
+ it('should remove the builtin metric when removing its last selected node', () => {
+ // Arrange
+ act(() => {
+ useEvaluationStore.getState().addBuiltinMetric(resourceType, resourceId, 'answer-correctness', [
+ { node_id: 'node-answer', title: 'Answer Node', type: 'llm' },
+ ])
+ })
+
+ // Act
+ renderMetricSection()
+ fireEvent.click(screen.getByRole('button', { name: 'Answer Node' }))
+
+ // Assert
+ expect(screen.queryByText('Answer Correctness')).not.toBeInTheDocument()
+ expect(useEvaluationStore.getState().resources[`${resourceType}:${resourceId}`]!.metrics).toHaveLength(0)
+ })
+
it('should show only unselected nodes in the add-node dropdown and append the selected node', () => {
// Arrange
mockUseDefaultEvaluationMetrics.mockReturnValue({
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
index eb661e3a14..8013ceb2af 100644
--- a/web/app/components/evaluation/components/metric-section/builtin-metric-card.tsx
+++ b/web/app/components/evaluation/components/metric-section/builtin-metric-card.tsx
@@ -39,6 +39,16 @@ const BuiltinMetricCard = ({
? availableNodeInfoList.filter(nodeInfo => !selectedNodeIdSet.has(nodeInfo.node_id))
: []
const shouldShowAddNode = selectableNodeInfoList.length > 0
+ const handleRemoveNode = (nodeId: string) => {
+ const nextSelectedNodeInfoList = selectedNodeInfoList.filter(item => item.node_id !== nodeId)
+
+ if (nextSelectedNodeInfoList.length === 0) {
+ removeMetric(resourceType, resourceId, metric.id)
+ return
+ }
+
+ updateBuiltinMetric(resourceType, resourceId, metric.optionId, nextSelectedNodeInfoList)
+ }
return (
@@ -92,12 +102,7 @@ const BuiltinMetricCard = ({
type="button"
className="flex h-4 w-4 items-center justify-center rounded-sm text-text-quaternary transition-colors hover:text-text-secondary"
aria-label={nodeInfo.title}
- onClick={() => updateBuiltinMetric(
- resourceType,
- resourceId,
- metric.optionId,
- selectedNodeInfoList.filter(item => item.node_id !== nodeInfo.node_id),
- )}
+ onClick={() => handleRemoveNode(nodeInfo.node_id)}
>
diff --git a/web/app/components/evaluation/components/metric-selector/index.tsx b/web/app/components/evaluation/components/metric-selector/index.tsx
index 68ad67d343..10ccc5b76a 100644
--- a/web/app/components/evaluation/components/metric-selector/index.tsx
+++ b/web/app/components/evaluation/components/metric-selector/index.tsx
@@ -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 = ({
-
- {t('metrics.add')}
-
- )
- : (
-
- )
+
)}
/>
diff --git a/web/app/components/evaluation/components/metric-selector/types.ts b/web/app/components/evaluation/components/metric-selector/types.ts
index 11f7518257..1acaa3cee0 100644
--- a/web/app/components/evaluation/components/metric-selector/types.ts
+++ b/web/app/components/evaluation/components/metric-selector/types.ts
@@ -3,7 +3,6 @@ import type { NodeInfo } from '@/types/evaluation'
export type MetricSelectorProps = NonPipelineEvaluationResourceProps & {
triggerClassName?: string
- triggerStyle?: 'button' | 'text'
}
export type MetricVisualTone = 'indigo' | 'green'
diff --git a/web/app/components/evaluation/components/pipeline/pipeline-batch-actions.tsx b/web/app/components/evaluation/components/pipeline/pipeline-batch-actions.tsx
index cad35dc31b..123c654f8e 100644
--- a/web/app/components/evaluation/components/pipeline/pipeline-batch-actions.tsx
+++ b/web/app/components/evaluation/components/pipeline/pipeline-batch-actions.tsx
@@ -58,7 +58,6 @@ const PipelineBatchActions = ({
isRunning={actions.isRunning}
onUploadFile={actions.handleUploadFile}
onClearUploadedFile={actions.handleClearUploadedFile}
- onDownloadTemplate={actions.handleDownloadTemplate}
onRun={actions.handleRun}
/>
diff --git a/web/app/components/evaluation/types.ts b/web/app/components/evaluation/types.ts
index c57d696b79..455869b0ba 100644
--- a/web/app/components/evaluation/types.ts
+++ b/web/app/components/evaluation/types.ts
@@ -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 = {
diff --git a/web/app/components/evaluation/utils.ts b/web/app/components/evaluation/utils.ts
index 3fba8158d6..c67937cb13 100644
--- a/web/app/components/evaluation/utils.ts
+++ b/web/app/components/evaluation/utils.ts
@@ -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),
diff --git a/web/i18n/en-US/evaluation.json b/web/i18n/en-US/evaluation.json
index dfdde63e26..acdcbe54c6 100644
--- a/web/i18n/en-US/evaluation.json
+++ b/web/i18n/en-US/evaluation.json
@@ -1,11 +1,12 @@
{
"batch.description": "Execute batch evaluations and track performance history.",
- "batch.downloadTemplate": "Download Excel Template",
+ "batch.downloadTemplate": "Download CSV Template",
"batch.emptyHistory": "No test history yet.",
"batch.example": "Example:",
"batch.fileRequired": "Upload an evaluation dataset file before running the test.",
"batch.loadingInputFields": "Loading input fields...",
"batch.noInputFields": "No published start node input fields found.",
+ "batch.noSnippetInputFields": "No published snippet input fields found.",
"batch.noticeDescription": "Configuration incomplete. Select the Judge Model and Metrics on the left to generate your batch test template.",
"batch.noticeTitle": "Quick start",
"batch.removeUploadedFile": "Remove uploaded file",
@@ -21,13 +22,12 @@
"batch.tabs.input-fields": "Input Fields",
"batch.title": "Batch Test",
"batch.uploadAndRun": "Upload & Run Test",
- "batch.uploadDropzoneDownloadLink": "here",
- "batch.uploadDropzoneDownloadPrefix": "Don't have the template? Download",
"batch.uploadDropzoneEmphasis": "filled",
"batch.uploadDropzonePrefix": "Drag and drop your",
- "batch.uploadDropzoneSuffix": "Excel Template",
+ "batch.uploadDropzoneSuffix": "CSV Template",
+ "batch.uploadDropzoneUploadButton": "Upload file",
"batch.uploadError": "Failed to upload file.",
- "batch.uploadHint": "Select a .csv or .xlsx file",
+ "batch.uploadHint": "Select a .csv file",
"batch.uploadTitle": "Upload test file",
"batch.uploading": "Uploading file...",
"batch.validation": "Complete the judge model, metrics, and custom mappings before running a batch test.",
diff --git a/web/i18n/zh-Hans/evaluation.json b/web/i18n/zh-Hans/evaluation.json
index 7ca9b76874..c0972282c6 100644
--- a/web/i18n/zh-Hans/evaluation.json
+++ b/web/i18n/zh-Hans/evaluation.json
@@ -1,11 +1,12 @@
{
"batch.description": "执行批量评测并追踪性能历史。",
- "batch.downloadTemplate": "下载 Excel 模板",
+ "batch.downloadTemplate": "下载 CSV 模板",
"batch.emptyHistory": "还没有测试历史。",
"batch.example": "示例:",
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
"batch.loadingInputFields": "正在加载输入字段...",
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
+ "batch.noSnippetInputFields": "未找到已发布的片段输入字段。",
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
"batch.noticeTitle": "快速开始",
"batch.removeUploadedFile": "移除已上传文件",
@@ -21,13 +22,12 @@
"batch.tabs.input-fields": "输入字段",
"batch.title": "批量测试",
"batch.uploadAndRun": "上传并运行测试",
- "batch.uploadDropzoneDownloadLink": "下载",
- "batch.uploadDropzoneDownloadPrefix": "还没有模板?",
"batch.uploadDropzoneEmphasis": "已填写的",
"batch.uploadDropzonePrefix": "拖拽你的",
- "batch.uploadDropzoneSuffix": "Excel 模板",
+ "batch.uploadDropzoneSuffix": "CSV 模板",
+ "batch.uploadDropzoneUploadButton": "上传文件",
"batch.uploadError": "文件上传失败。",
- "batch.uploadHint": "选择 .csv 或 .xlsx 文件",
+ "batch.uploadHint": "选择 .csv 文件",
"batch.uploadTitle": "上传测试文件",
"batch.uploading": "文件上传中...",
"batch.validation": "运行批量测试前,请先完成判定模型、指标和自定义映射配置。",
diff --git a/web/next.config.ts b/web/next.config.ts
index db44f5b9ed..a1c2e410a1 100644
--- a/web/next.config.ts
+++ b/web/next.config.ts
@@ -5,9 +5,13 @@ import { env } from './env'
const isDev = process.env.NODE_ENV === 'development'
const withMDX = createMDX()
+const allowedDevOrigins = process.env.NEXT_ALLOWED_DEV_ORIGINS?.split(',')
+ .map(origin => origin.trim())
+ .filter(Boolean)
const nextConfig: NextConfig = {
basePath: env.NEXT_PUBLIC_BASE_PATH,
+ ...(allowedDevOrigins?.length ? { allowedDevOrigins } : {}),
transpilePackages: ['@t3-oss/env-core', '@t3-oss/env-nextjs', 'echarts', 'zrender'],
turbopack: {
rules: codeInspectorPlugin({