mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
feat(web): use api to fetch template
This commit is contained in:
parent
f00f8e020f
commit
8a72e46ce8
@ -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()
|
||||
})
|
||||
|
||||
@ -23,7 +23,6 @@ const InputFieldsTab = ({
|
||||
const actions = useInputFieldsActions({
|
||||
resourceType,
|
||||
resourceId,
|
||||
inputFields,
|
||||
isInputFieldsLoading,
|
||||
isPanelReady,
|
||||
isRunnable,
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "已填写的",
|
||||
|
||||
@ -131,6 +131,10 @@ export const useStartEvaluationRunMutation = () => {
|
||||
}))
|
||||
}
|
||||
|
||||
export const useEvaluationTemplateColumnsMutation = () => {
|
||||
return useMutation(consoleQuery.evaluation.templateColumns.mutationOptions())
|
||||
}
|
||||
|
||||
export const useAvailableEvaluationWorkflows = (
|
||||
params: AvailableEvaluationWorkflowsParams = {},
|
||||
options?: { enabled?: boolean },
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user