feat(web): use api to fetch template

This commit is contained in:
JzoNg 2026-05-05 20:27:35 +08:00
parent f00f8e020f
commit 8a72e46ce8
12 changed files with 95 additions and 50 deletions

View File

@ -11,6 +11,7 @@ const mockUseDefaultEvaluationMetrics = vi.hoisted(() => vi.fn())
const mockUseEvaluationConfig = vi.hoisted(() => vi.fn())
const mockUseSaveEvaluationConfigMutation = vi.hoisted(() => vi.fn())
const mockUseStartEvaluationRunMutation = vi.hoisted(() => vi.fn())
const mockUseEvaluationTemplateColumnsMutation = vi.hoisted(() => vi.fn())
const mockUsePublishedPipelineInfo = vi.hoisted(() => vi.fn())
const mockUseSnippetPublishedWorkflow = vi.hoisted(() => vi.fn())
@ -55,6 +56,7 @@ vi.mock('@/service/use-evaluation', () => ({
useDefaultEvaluationMetrics: (...args: unknown[]) => mockUseDefaultEvaluationMetrics(...args),
useSaveEvaluationConfigMutation: (...args: unknown[]) => mockUseSaveEvaluationConfigMutation(...args),
useStartEvaluationRunMutation: (...args: unknown[]) => mockUseStartEvaluationRunMutation(...args),
useEvaluationTemplateColumnsMutation: (...args: unknown[]) => mockUseEvaluationTemplateColumnsMutation(...args),
}))
vi.mock('@/service/use-pipeline', () => ({
@ -170,6 +172,10 @@ describe('Evaluation', () => {
isPending: false,
mutate: vi.fn(),
})
mockUseEvaluationTemplateColumnsMutation.mockReturnValue({
isPending: false,
mutate: vi.fn(),
})
mockUsePublishedPipelineInfo.mockReturnValue({
data: {
graph: {
@ -624,6 +630,15 @@ describe('Evaluation', () => {
it('should download the fixed pipeline template columns', () => {
const createElement = document.createElement.bind(document)
const getTemplateColumns = vi.fn((_input: unknown, options?: { onSuccess?: (value: { columns: string[] }) => void }) => {
options?.onSuccess?.({
columns: ['index', 'query', 'expected_output'],
})
})
mockUseEvaluationTemplateColumnsMutation.mockReturnValue({
isPending: false,
mutate: getTemplateColumns,
})
let downloadLink: HTMLAnchorElement | undefined
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName, options) => {
const element = createElement(tagName, options)
@ -645,6 +660,16 @@ describe('Evaluation', () => {
const templateContent = decodeURIComponent(downloadLink?.href ?? '').replace('data:text/csv;charset=utf-8,', '')
expect(downloadLink?.download).toBe('pipeline-evaluation-template.csv')
expect(templateContent.trim().split(',')).toEqual(['index', 'query', 'expected_output'])
expect(getTemplateColumns).toHaveBeenCalledWith({
params: {
targetType: 'datasets',
targetId: 'dataset-template',
},
body: expect.objectContaining({
evaluation_model: 'gpt-4o-mini',
evaluation_model_provider: 'openai',
}),
}, expect.any(Object))
createElementSpy.mockRestore()
})

View File

@ -23,7 +23,6 @@ const InputFieldsTab = ({
const actions = useInputFieldsActions({
resourceType,
resourceId,
inputFields,
isInputFieldsLoading,
isPanelReady,
isRunnable,

View File

@ -2,32 +2,19 @@ import { buildTemplateCsvContent, getExampleValue } from '../input-fields-utils'
describe('input fields utils', () => {
describe('buildTemplateCsvContent', () => {
it('should use index as the first CSV column and append expected_output as the last CSV column', () => {
it('should build CSV content from API columns without injecting columns', () => {
expect(buildTemplateCsvContent([
{ name: 'query', type: 'string' },
{ name: 'context', type: 'string' },
])).toBe('index,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' },
'index',
'query',
'expected_output',
])).toBe('index,query,expected_output\n')
})
it('should not duplicate index when it already exists', () => {
it('should escape CSV column names', () => {
expect(buildTemplateCsvContent([
{ name: 'query', type: 'string' },
{ name: 'index', type: 'number' },
])).toBe('index,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('index,"query,text","answer ""draft""",expected_output\n')
'query,text',
'answer "draft"',
])).toBe('"query,text","answer ""draft"""\n')
})
})

View File

@ -11,7 +11,6 @@ export type InputField = {
}
export const INDEX_FIELD_NAME = 'index'
export const EXPECTED_OUTPUT_FIELD_NAME = 'expected_output'
export const getGraphNodes = (graph?: Record<string, unknown>) => {
return Array.isArray(graph?.nodes) ? graph.nodes as Node[] : []
@ -65,15 +64,8 @@ const escapeCsvCell = (value: string) => {
return `"${value.replace(/"/g, '""')}"`
}
export const buildTemplateCsvContent = (inputFields: InputField[]) => {
const fieldNames = inputFields
.map(field => field.name)
.filter(name => name !== INDEX_FIELD_NAME)
const templateFieldNames = fieldNames.includes(EXPECTED_OUTPUT_FIELD_NAME)
? [INDEX_FIELD_NAME, ...fieldNames]
: [INDEX_FIELD_NAME, ...fieldNames, EXPECTED_OUTPUT_FIELD_NAME]
return `${templateFieldNames.map(escapeCsvCell).join(',')}\n`
export const buildTemplateCsvContent = (columns: string[]) => {
return `${columns.map(escapeCsvCell).join(',')}\n`
}
export const getFileExtension = (fileName: string) => {

View File

@ -1,14 +1,13 @@
import type { EvaluationResourceProps } from '../../../types'
import type { InputField } from './input-fields-utils'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { upload } from '@/service/base'
import { useStartEvaluationRunMutation } from '@/service/use-evaluation'
import { useEvaluationTemplateColumnsMutation, useStartEvaluationRunMutation } from '@/service/use-evaluation'
import { formatFileSize } from '@/utils/format'
import { useEvaluationResource, useEvaluationStore } from '../../../store'
import { buildEvaluationRunRequest } from '../../../store-utils'
import { buildEvaluationConfigPayload, buildEvaluationRunRequest } from '../../../store-utils'
import { buildTemplateCsvContent, getFileExtension } from './input-fields-utils'
type UploadedFileMeta = {
@ -17,22 +16,18 @@ type UploadedFileMeta = {
}
type UseInputFieldsActionsParams = EvaluationResourceProps & {
inputFields: InputField[]
isInputFieldsLoading: boolean
isPanelReady: boolean
isRunnable: boolean
templateContent?: string
templateFileName: string
}
export const useInputFieldsActions = ({
resourceType,
resourceId,
inputFields,
isInputFieldsLoading,
isPanelReady,
isRunnable,
templateContent,
templateFileName,
}: UseInputFieldsActionsParams) => {
const { t } = useTranslation('evaluation')
@ -42,6 +37,7 @@ export const useInputFieldsActions = ({
const setUploadedFile = useEvaluationStore(state => state.setUploadedFile)
const setUploadedFileName = useEvaluationStore(state => state.setUploadedFileName)
const startRunMutation = useStartEvaluationRunMutation()
const templateColumnsMutation = useEvaluationTemplateColumnsMutation()
const [isUploadPopoverOpen, setIsUploadPopoverOpen] = useState(false)
const [uploadedFileMeta, setUploadedFileMeta] = useState<UploadedFileMeta | null>(null)
const uploadMutation = useMutation({
@ -71,21 +67,41 @@ export const useInputFieldsActions = ({
const isRunning = startRunMutation.isPending
const uploadedFileId = resource.uploadedFileId
const currentFileName = uploadedFileMeta?.name ?? resource.uploadedFileName
const canDownloadTemplate = isPanelReady && !isInputFieldsLoading && inputFields.length > 0
const canDownloadTemplate = isPanelReady && !templateColumnsMutation.isPending
const isRunDisabled = !isRunnable || !uploadedFileId || isFileUploading || isRunning
const uploadButtonDisabled = !isPanelReady || isInputFieldsLoading || isRunning
const handleDownloadTemplate = () => {
if (!inputFields.length) {
toast.warning(t('batch.noInputFields'))
const body = buildEvaluationConfigPayload(resource, resourceType)
if (!body) {
toast.warning(t('batch.validation'))
return
}
const content = templateContent ?? buildTemplateCsvContent(inputFields)
const link = document.createElement('a')
link.href = `data:text/csv;charset=utf-8,${encodeURIComponent(content)}`
link.download = templateFileName
link.click()
templateColumnsMutation.mutate({
params: {
targetType: resourceType,
targetId: resourceId,
},
body,
}, {
onSuccess: ({ columns }) => {
if (!columns.length) {
toast.warning(t('batch.noTemplateColumns'))
return
}
const content = buildTemplateCsvContent(columns)
const link = document.createElement('a')
link.href = `data:text/csv;charset=utf-8,${encodeURIComponent(content)}`
link.download = templateFileName
link.click()
},
onError: () => {
toast.error(t('batch.templateColumnsError'))
},
})
}
const handleRun = () => {

View File

@ -14,7 +14,6 @@ const PIPELINE_INPUT_FIELDS: InputField[] = [
{ name: 'query', type: 'string' },
{ name: 'expected_output', type: 'string' },
]
const PIPELINE_TEMPLATE_CONTENT = 'index,query,expected_output\n'
const PipelineBatchActions = ({
resourceType,
@ -27,11 +26,9 @@ const PipelineBatchActions = ({
const actions = useInputFieldsActions({
resourceType,
resourceId,
inputFields: PIPELINE_INPUT_FIELDS,
isInputFieldsLoading: false,
isPanelReady: isConfigReady,
isRunnable,
templateContent: PIPELINE_TEMPLATE_CONTENT,
templateFileName: EVALUATION_TEMPLATE_FILE_NAMES[resourceType],
})

View File

@ -14,6 +14,7 @@ import type {
EvaluationRunDetailResponse,
EvaluationRunRequest,
EvaluationTargetType,
EvaluationTemplateColumnsResponse,
EvaluationVersionDetailResponse,
EvaluationWorkflowAssociatedTargetsResponse,
} from '@/types/evaluation'
@ -154,6 +155,20 @@ export const evaluationTemplateDownloadContract = base
}>())
.output(type<unknown>())
export const evaluationTemplateColumnsContract = base
.route({
path: '/{targetType}/{targetId}/evaluation/template-columns',
method: 'POST',
})
.input(type<{
params: {
targetType: EvaluationTargetType
targetId: string
}
body: EvaluationConfigData
}>())
.output(type<EvaluationTemplateColumnsResponse>())
export const evaluationConfigContract = base
.route({
path: '/{targetType}/{targetId}/evaluation',

View File

@ -20,6 +20,7 @@ import {
evaluationMetricsContract,
evaluationNodeInfoContract,
evaluationRunDetailContract,
evaluationTemplateColumnsContract,
evaluationTemplateDownloadContract,
evaluationVersionDetailContract,
evaluationWorkflowAssociatedTargetsContract,
@ -140,6 +141,7 @@ export const consoleRouterContract = {
},
evaluation: {
templateDownload: evaluationTemplateDownloadContract,
templateColumns: evaluationTemplateColumnsContract,
config: evaluationConfigContract,
saveConfig: saveEvaluationConfigContract,
logs: evaluationLogsContract,

View File

@ -7,6 +7,7 @@
"batch.loadingInputFields": "Loading input fields...",
"batch.noInputFields": "No published start node input fields found.",
"batch.noSnippetInputFields": "No published snippet input fields found.",
"batch.noTemplateColumns": "No template columns 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",
@ -20,6 +21,7 @@
"batch.status.success": "Success",
"batch.tabs.history": "Test History",
"batch.tabs.input-fields": "Input Fields",
"batch.templateColumnsError": "Failed to generate the CSV template.",
"batch.title": "Batch Test",
"batch.uploadAndRun": "Upload & Run Test",
"batch.uploadDropzoneEmphasis": "filled",

View File

@ -7,6 +7,7 @@
"batch.loadingInputFields": "正在加载输入字段...",
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
"batch.noSnippetInputFields": "未找到已发布的片段输入字段。",
"batch.noTemplateColumns": "未找到模板列。",
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
"batch.noticeTitle": "快速开始",
"batch.removeUploadedFile": "移除已上传文件",
@ -20,6 +21,7 @@
"batch.status.success": "成功",
"batch.tabs.history": "测试历史",
"batch.tabs.input-fields": "输入字段",
"batch.templateColumnsError": "生成 CSV 模板失败。",
"batch.title": "批量测试",
"batch.uploadAndRun": "上传并运行测试",
"batch.uploadDropzoneEmphasis": "已填写的",

View File

@ -131,6 +131,10 @@ export const useStartEvaluationRunMutation = () => {
}))
}
export const useEvaluationTemplateColumnsMutation = () => {
return useMutation(consoleQuery.evaluation.templateColumns.mutationOptions())
}
export const useAvailableEvaluationWorkflows = (
params: AvailableEvaluationWorkflowsParams = {},
options?: { enabled?: boolean },

View File

@ -59,6 +59,10 @@ export type EvaluationRunRequest = EvaluationConfigData & {
file_id: string
}
export type EvaluationTemplateColumnsResponse = {
columns: string[]
}
export type EvaluationRunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
export type EvaluationJudgmentMetricsSummary = {