diff --git a/web/app/components/evaluation/__tests__/index.spec.tsx b/web/app/components/evaluation/__tests__/index.spec.tsx index b451223390..3d1ae351d3 100644 --- a/web/app/components/evaluation/__tests__/index.spec.tsx +++ b/web/app/components/evaluation/__tests__/index.spec.tsx @@ -12,6 +12,7 @@ const mockUseEvaluationConfig = vi.hoisted(() => vi.fn()) const mockUseSaveEvaluationConfigMutation = vi.hoisted(() => vi.fn()) const mockUseStartEvaluationRunMutation = vi.hoisted(() => vi.fn()) const mockUsePublishedPipelineInfo = vi.hoisted(() => vi.fn()) +const mockUseSnippetPublishedWorkflow = vi.hoisted(() => vi.fn()) vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useModelList: () => ({ @@ -86,23 +87,7 @@ vi.mock('@/service/use-workflow', () => ({ })) vi.mock('@/service/use-snippet-workflows', () => ({ - useSnippetPublishedWorkflow: () => ({ - data: { - graph: { - nodes: [{ - id: 'start', - data: { - type: 'start', - variables: [{ - variable: 'query', - type: 'text-input', - }], - }, - }], - }, - }, - isLoading: false, - }), + useSnippetPublishedWorkflow: (...args: unknown[]) => mockUseSnippetPublishedWorkflow(...args), })) const renderWithQueryClient = (ui: ReactNode) => { @@ -199,6 +184,24 @@ describe('Evaluation', () => { }, }, }) + mockUseSnippetPublishedWorkflow.mockReturnValue({ + data: { + graph: { + nodes: [{ + id: 'start', + data: { + type: 'start', + variables: [{ + variable: 'query', + type: 'text-input', + }], + }, + }], + }, + input_fields: [], + }, + isLoading: false, + }) mockUpload.mockResolvedValue({ id: 'uploaded-file-id', name: 'evaluation.csv', @@ -305,6 +308,92 @@ describe('Evaluation', () => { expect(resetButton).toBeDisabled() }) + it('should hide the batch config warning when judge model and metrics are configured', () => { + const resourceType = 'apps' + const resourceId = 'app-batch-configured' + const store = useEvaluationStore.getState() + + act(() => { + store.ensureResource(resourceType, resourceId) + store.setJudgeModel(resourceType, resourceId, 'openai::gpt-4o-mini') + store.addBuiltinMetric(resourceType, resourceId, 'faithfulness', [ + { node_id: 'node-faithfulness', title: 'Retriever Node', type: 'retriever' }, + ]) + }) + + renderWithQueryClient() + + expect(screen.queryByText('evaluation.batch.noticeDescription')).not.toBeInTheDocument() + }) + + it('should use published snippet input fields for snippet batch templates', () => { + mockUseSnippetPublishedWorkflow.mockReturnValue({ + data: { + graph: { + nodes: [{ + id: 'start', + data: { + type: 'start', + variables: [{ + variable: 'graph_only', + type: 'text-input', + }], + }, + }], + }, + input_fields: [ + { + label: 'Snippet Topic', + variable: 'snippet_topic', + type: 'text-input', + required: true, + }, + { + label: 'Need Summary', + variable: 'need_summary', + type: 'checkbox', + required: false, + }, + ], + }, + isLoading: false, + }) + + renderWithQueryClient() + + expect(mockUseSnippetPublishedWorkflow).toHaveBeenCalledWith('snippet-fields') + expect(screen.getByText('snippet_topic')).toBeInTheDocument() + expect(screen.getByText('need_summary')).toBeInTheDocument() + expect(screen.queryByText('graph_only')).not.toBeInTheDocument() + }) + + it('should show snippet-specific empty input fields copy', () => { + mockUseSnippetPublishedWorkflow.mockReturnValue({ + data: { + graph: { + nodes: [{ + id: 'start', + data: { + type: 'start', + variables: [{ + variable: 'graph_only', + type: 'text-input', + }], + }, + }], + }, + input_fields: [], + }, + isLoading: false, + }) + + renderWithQueryClient() + + expect(screen.getByText('evaluation.batch.noSnippetInputFields')).toBeInTheDocument() + expect(screen.queryByText('evaluation.batch.noInputFields')).not.toBeInTheDocument() + expect(screen.queryByText('graph_only')).not.toBeInTheDocument() + }) + it('should hide the value row for empty operators', () => { const resourceType = 'apps' const resourceId = 'app-2' @@ -366,7 +455,7 @@ describe('Evaluation', () => { render() - fireEvent.click(screen.getByRole('combobox', { name: 'evaluation.conditions.addCondition' })) + fireEvent.click(screen.getByRole('button', { name: 'evaluation.conditions.addCondition' })) expect(screen.getByText('Faithfulness')).toBeInTheDocument() expect(screen.getByText('Review Workflow')).toBeInTheDocument() @@ -375,7 +464,7 @@ describe('Evaluation', () => { expect(screen.getByText('evaluation.conditions.valueTypes.number')).toBeInTheDocument() expect(screen.getByText('evaluation.conditions.valueTypes.string')).toBeInTheDocument() - fireEvent.click(screen.getByRole('option', { name: /reason/i })) + fireEvent.click(screen.getByRole('menuitem', { name: /reason/i })) const condition = useEvaluationStore.getState().resources['apps:app-conditions-dropdown'].judgmentConfig.conditions[0] @@ -552,7 +641,7 @@ describe('Evaluation', () => { expect(screen.getAllByText('query').length).toBeGreaterThan(0) expect(screen.getAllByText('Expect Results').length).toBeGreaterThan(0) - const fileInput = document.querySelector('input[type="file"][accept=".csv,.xlsx"]') + const fileInput = document.querySelector('input[type="file"][accept=".csv"]') expect(fileInput).toBeInTheDocument() fireEvent.change(fileInput!, { diff --git a/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx b/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx index 732b4df76c..0819ff2137 100644 --- a/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/history-tab.tsx @@ -181,7 +181,7 @@ const HistoryTab = ({ {!isInitialLoading && records.length === 0 && ( -
+
{t('history.empty')}
)} diff --git a/web/app/components/evaluation/components/batch-test-panel/index.tsx b/web/app/components/evaluation/components/batch-test-panel/index.tsx index 8b4d9ca98a..2de6aee712 100644 --- a/web/app/components/evaluation/components/batch-test-panel/index.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/index.tsx @@ -22,7 +22,7 @@ const BatchTestPanel = ({ const resource = useEvaluationResource(resourceType, resourceId) const setBatchTab = useEvaluationStore(state => state.setBatchTab) const isRunnable = isEvaluationRunnable(resource) - const isPanelReady = !!resource.judgeModelId && resource.metrics.length > 0 + const hasBatchConfig = !!resource.judgeModelId && resource.metrics.length > 0 return (
@@ -31,12 +31,14 @@ const BatchTestPanel = ({
{t('batch.title')}
{t('batch.description')}
-
-
-
+ )}
@@ -56,12 +58,12 @@ const BatchTestPanel = ({ ))}
-
+
{resource.activeBatchTab === 'input-fields' && ( )} diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx b/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx index 5cff6e3dc0..a305be4561 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields-tab.tsx @@ -33,6 +33,7 @@ const InputFieldsTab = ({ return (
@@ -54,7 +55,6 @@ const InputFieldsTab = ({ isRunning={actions.isRunning} onUploadFile={actions.handleUploadFile} onClearUploadedFile={actions.handleClearUploadedFile} - onDownloadTemplate={actions.handleDownloadTemplate} onRun={actions.handleRun} />
diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/__tests__/input-fields-utils.spec.ts b/web/app/components/evaluation/components/batch-test-panel/input-fields/__tests__/input-fields-utils.spec.ts new file mode 100644 index 0000000000..67ee548738 --- /dev/null +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/__tests__/input-fields-utils.spec.ts @@ -0,0 +1,26 @@ +import { buildTemplateCsvContent } from '../input-fields-utils' + +describe('input fields utils', () => { + describe('buildTemplateCsvContent', () => { + it('should append expected_output as the last CSV column', () => { + expect(buildTemplateCsvContent([ + { name: 'query', type: 'string' }, + { name: 'context', type: 'string' }, + ])).toBe('query,context,expected_output\n') + }) + + it('should not duplicate expected_output when it already exists', () => { + expect(buildTemplateCsvContent([ + { name: 'query', type: 'string' }, + { name: 'expected_output', type: 'string' }, + ])).toBe('query,expected_output\n') + }) + + it('should escape CSV column names before appending expected_output', () => { + expect(buildTemplateCsvContent([ + { name: 'query,text', type: 'string' }, + { name: 'answer "draft"', type: 'string' }, + ])).toBe('"query,text","answer ""draft""",expected_output\n') + }) + }) +}) diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx index 83201ea5a7..2169bd6464 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-requirements.tsx @@ -1,16 +1,22 @@ +import type { EvaluationResourceType } from '../../../types' import type { InputField } from './input-fields-utils' import { useTranslation } from 'react-i18next' type InputFieldsRequirementsProps = { + resourceType: EvaluationResourceType inputFields: InputField[] isLoading: boolean } const InputFieldsRequirements = ({ + resourceType, inputFields, isLoading, }: InputFieldsRequirementsProps) => { const { t } = useTranslation('evaluation') + const emptyDescription = resourceType === 'snippets' + ? t('batch.noSnippetInputFields') + : t('batch.noInputFields') return (
@@ -24,7 +30,7 @@ const InputFieldsRequirements = ({ )} {!isLoading && inputFields.length === 0 && (
- {t('batch.noInputFields')} + {emptyDescription}
)} {!isLoading && inputFields.map(field => ( diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts index 5a71b81d06..6daae07a84 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/input-fields-utils.ts @@ -1,13 +1,17 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import type { InputVar, Node } from '@/app/components/workflow/types' +import type { SnippetInputField } from '@/types/snippet' import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' +import { PipelineInputVarType } from '@/models/pipeline' export type InputField = { name: string type: string } +export const EXPECTED_OUTPUT_FIELD_NAME = 'expected_output' + export const getGraphNodes = (graph?: Record) => { return Array.isArray(graph?.nodes) ? graph.nodes as Node[] : [] } @@ -27,6 +31,32 @@ export const getStartNodeInputFields = (nodes?: Node[]): InputField[] => { })) } +const PIPELINE_INPUT_VAR_TYPE_TO_FIELD_TYPE: Record = { + [PipelineInputVarType.textInput]: 'string', + [PipelineInputVarType.paragraph]: 'string', + [PipelineInputVarType.select]: 'string', + [PipelineInputVarType.number]: 'number', + [PipelineInputVarType.singleFile]: 'file', + [PipelineInputVarType.multiFiles]: 'array[file]', + [PipelineInputVarType.checkbox]: 'boolean', +} + +export const getSnippetInputFields = (fields?: SnippetInputField[]): InputField[] => { + if (!Array.isArray(fields)) + return [] + + return fields + .filter((field): field is SnippetInputField & { variable: string } => + typeof field.variable === 'string' && !!field.variable, + ) + .map(field => ({ + name: field.variable, + type: typeof field.type === 'string' && field.type in PIPELINE_INPUT_VAR_TYPE_TO_FIELD_TYPE + ? PIPELINE_INPUT_VAR_TYPE_TO_FIELD_TYPE[field.type as PipelineInputVarType] + : 'string', + })) +} + const escapeCsvCell = (value: string) => { if (!/[",\n\r]/.test(value)) return value @@ -35,7 +65,12 @@ const escapeCsvCell = (value: string) => { } export const buildTemplateCsvContent = (inputFields: InputField[]) => { - return `${inputFields.map(field => escapeCsvCell(field.name)).join(',')}\n` + const fieldNames = inputFields.map(field => field.name) + const templateFieldNames = fieldNames.includes(EXPECTED_OUTPUT_FIELD_NAME) + ? fieldNames + : [...fieldNames, EXPECTED_OUTPUT_FIELD_NAME] + + return `${templateFieldNames.map(escapeCsvCell).join(',')}\n` } export const getFileExtension = (fileName: string) => { diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/upload-run-popover.tsx b/web/app/components/evaluation/components/batch-test-panel/input-fields/upload-run-popover.tsx index a9c293297d..014061c46e 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/upload-run-popover.tsx +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/upload-run-popover.tsx @@ -25,7 +25,6 @@ type UploadRunPopoverProps = { isRunning: boolean onUploadFile: (file: File | undefined) => void onClearUploadedFile: () => void - onDownloadTemplate: () => void onRun: () => void } @@ -43,7 +42,6 @@ const UploadRunPopover = ({ isRunning, onUploadFile, onClearUploadedFile, - onDownloadTemplate, onRun, }: UploadRunPopoverProps) => { const { t } = useTranslation('evaluation') @@ -82,7 +80,7 @@ const UploadRunPopover = ({ ref={fileInputRef} hidden type="file" - accept=".csv,.xlsx" + accept=".csv" onChange={handleFileChange} /> {currentFileName @@ -111,7 +109,7 @@ const UploadRunPopover = ({ onClick={onClearUploadedFile} aria-label={t('batch.removeUploadedFile')} > -
@@ -139,15 +137,15 @@ const UploadRunPopover = ({ {t('batch.uploadDropzoneSuffix')}
- {t('batch.uploadDropzoneDownloadPrefix')} - {' '} + {' '} + {t('batch.uploadHint')}
diff --git a/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts b/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts index a319603026..d56507c3ac 100644 --- a/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts +++ b/web/app/components/evaluation/components/batch-test-panel/input-fields/use-published-input-fields.ts @@ -2,7 +2,7 @@ import type { EvaluationResourceType } from '../../../types' import { useMemo } from 'react' import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows' import { useAppWorkflow } from '@/service/use-workflow' -import { getGraphNodes, getStartNodeInputFields } from './input-fields-utils' +import { getSnippetInputFields, getStartNodeInputFields } from './input-fields-utils' export const usePublishedInputFields = ( resourceType: EvaluationResourceType, @@ -16,10 +16,10 @@ export const usePublishedInputFields = ( return getStartNodeInputFields(currentAppWorkflow?.graph.nodes) if (resourceType === 'snippets') - return getStartNodeInputFields(getGraphNodes(currentSnippetWorkflow?.graph)) + return getSnippetInputFields(currentSnippetWorkflow?.input_fields) return [] - }, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.graph, resourceType]) + }, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.input_fields, resourceType]) return { inputFields, diff --git a/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx b/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx index 2a17fadf83..c9bdb8125e 100644 --- a/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx +++ b/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx @@ -1,15 +1,12 @@ 'use client' import type { ConditionMetricOptionGroup, EvaluationResourceProps } from '../../types' -import { cn } from '@langgenius/dify-ui/cn' +import { Button } from '@langgenius/dify-ui/button' import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, -} from '@langgenius/dify-ui/select' + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useEvaluationStore } from '../../store' @@ -28,47 +25,59 @@ const AddConditionSelect = ({ }: AddConditionSelectProps) => { const { t } = useTranslation('evaluation') const addCondition = useEvaluationStore(state => state.addCondition) - const [selectKey, setSelectKey] = useState(0) + const [open, setOpen] = useState(false) + + const handleOpenChange = (nextOpen: boolean) => { + if (disabled) + return + + setOpen(nextOpen) + } return ( - + + ))} + + ))} + + + ) } diff --git a/web/app/components/evaluation/components/conditions-section/condition-group.tsx b/web/app/components/evaluation/components/conditions-section/condition-group.tsx index 15b9246834..078b94ef80 100644 --- a/web/app/components/evaluation/components/conditions-section/condition-group.tsx +++ b/web/app/components/evaluation/components/conditions-section/condition-group.tsx @@ -6,7 +6,6 @@ import type { EvaluationResourceProps, JudgmentConditionItem, } from '../../types' -import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Select, @@ -19,7 +18,9 @@ import { } from '@langgenius/dify-ui/select' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import ActionButton from '@/app/components/base/action-button' import Input from '@/app/components/base/input' +import BlockIcon from '@/app/components/workflow/block-icon' import { getAllowedOperators, requiresConditionValue, useEvaluationResource, useEvaluationStore } from '../../store' import { buildConditionMetricOptions, @@ -29,6 +30,7 @@ import { isSelectorEqual, serializeVariableSelector, } from '../../utils' +import { getEvaluationNodeBlockType } from '../metric-selector/utils' type ConditionMetricLabelProps = { metric?: ConditionMetricOption @@ -56,14 +58,8 @@ type ConditionValueInputProps = { type ConditionGroupProps = EvaluationResourceProps -const getMetricValueTypeIconClassName = (valueType: ConditionMetricOption['valueType']) => { - if (valueType === 'number') - return 'i-ri-hashtag' - - if (valueType === 'boolean') - return 'i-ri-checkbox-circle-line' - - return 'i-ri-bar-chart-box-line' +const getMetricVariableLabel = (variableName: string) => { + return variableName.replaceAll('-', '_') } const ConditionMetricLabel = ({ @@ -73,13 +69,28 @@ const ConditionMetricLabel = ({ if (!metric) return {placeholder} - return ( -
-
- - {metric.itemLabel} + if (metric.kind === 'builtin' && metric.nodeInfo) { + return ( +
+
+ {getMetricVariableLabel(metric.variableSelector[1])} + / + + + {metric.itemLabel} + + {metric.valueType} +
+
+ ) + } + + return ( +
+
+ {metric.itemLabel} + {metric.valueType}
- {metric.groupLabel}
) } @@ -114,7 +125,6 @@ const ConditionMetricSelect = ({ {group.options.map(option => (
- {option.itemLabel} {t(getConditionMetricValueTypeTranslationKey(option.valueType))} @@ -141,7 +151,7 @@ const ConditionOperatorSelect = ({ {getComparisonOperatorLabel(operator, t)} - + {operators.map(nextOperator => ( {getComparisonOperatorLabel(nextOperator, t)} @@ -212,88 +222,87 @@ const ConditionGroup = ({ const { t } = useTranslation('evaluation') const resource = useEvaluationResource(resourceType, resourceId) const metricOptions = useMemo(() => buildConditionMetricOptions(resource.metrics), [resource.metrics]) + const logicalOperator = resource.judgmentConfig.logicalOperator const logicalLabels = { and: t('conditions.logical.and'), or: t('conditions.logical.or'), } + const hasMultipleConditions = resource.judgmentConfig.conditions.length > 1 const setConditionLogicalOperator = useEvaluationStore(state => state.setConditionLogicalOperator) const removeCondition = useEvaluationStore(state => state.removeCondition) const updateConditionMetric = useEvaluationStore(state => state.updateConditionMetric) const updateConditionOperator = useEvaluationStore(state => state.updateConditionOperator) const updateConditionValue = useEvaluationStore(state => state.updateConditionValue) + const toggleLogicalOperator = () => { + setConditionLogicalOperator(resourceType, resourceId, logicalOperator === 'and' ? 'or' : 'and') + } return (
-
-
-
- {(['and', 'or'] as const).map(operator => ( - - ))} +
+ {hasMultipleConditions && ( +
+
+
+
-
-
+ )} -
- {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/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({