mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
Merge branch 'jzh' into deploy/dev
This commit is contained in:
commit
1d1b9d56c7
@ -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(<Evaluation resourceType={resourceType} resourceId={resourceId} />)
|
||||
|
||||
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(<Evaluation resourceType="snippets" resourceId="snippet-fields" />)
|
||||
|
||||
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(<Evaluation resourceType="snippets" resourceId="snippet-empty-fields" />)
|
||||
|
||||
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(<ConditionsSection resourceType={resourceType} resourceId={resourceId} />)
|
||||
|
||||
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<HTMLInputElement>('input[type="file"][accept=".csv,.xlsx"]')
|
||||
const fileInput = document.querySelector<HTMLInputElement>('input[type="file"][accept=".csv"]')
|
||||
expect(fileInput).toBeInTheDocument()
|
||||
|
||||
fireEvent.change(fileInput!, {
|
||||
|
||||
@ -181,7 +181,7 @@ const HistoryTab = ({
|
||||
</tbody>
|
||||
</table>
|
||||
{!isInitialLoading && records.length === 0 && (
|
||||
<div className="rounded-2xl border border-dashed border-divider-subtle px-4 py-10 text-center system-sm-regular text-text-tertiary">
|
||||
<div className="mt-4 rounded-2xl border border-dashed border-divider-subtle px-4 py-10 text-center system-sm-regular text-text-tertiary">
|
||||
{t('history.empty')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -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 (
|
||||
<div className="flex h-full min-h-0 flex-col bg-background-default">
|
||||
@ -31,12 +31,14 @@ const BatchTestPanel = ({
|
||||
<div className="system-xl-semibold text-text-primary">{t('batch.title')}</div>
|
||||
<div className="mt-1 system-sm-regular text-text-tertiary">{t('batch.description')}</div>
|
||||
</div>
|
||||
<div className="mt-4 rounded-xl border border-divider-subtle bg-components-card-bg p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span aria-hidden="true" className="mt-0.5 i-ri-alert-fill h-4 w-4 shrink-0 text-text-warning" />
|
||||
<div className="system-xs-regular text-text-tertiary">{t('batch.noticeDescription')}</div>
|
||||
{!hasBatchConfig && (
|
||||
<div className="mt-4 rounded-xl border border-divider-subtle bg-components-card-bg p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span aria-hidden="true" className="mt-0.5 i-ri-alert-fill h-4 w-4 shrink-0 text-text-warning" />
|
||||
<div className="system-xs-regular text-text-tertiary">{t('batch.noticeDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-b border-divider-subtle px-6">
|
||||
<div className="flex gap-4">
|
||||
@ -56,12 +58,12 @@ const BatchTestPanel = ({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('min-h-0 flex-1 overflow-y-auto px-6 py-4', !isPanelReady && 'opacity-50')}>
|
||||
<div className={cn('min-h-0 flex-1 overflow-y-auto px-6 py-4', !hasBatchConfig && 'opacity-50')}>
|
||||
{resource.activeBatchTab === 'input-fields' && (
|
||||
<InputFieldsTab
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
isPanelReady={isPanelReady}
|
||||
isPanelReady={hasBatchConfig}
|
||||
isRunnable={isRunnable}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -33,6 +33,7 @@ const InputFieldsTab = ({
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<InputFieldsRequirements
|
||||
resourceType={resourceType}
|
||||
inputFields={inputFields}
|
||||
isLoading={isInputFieldsLoading}
|
||||
/>
|
||||
@ -54,7 +55,6 @@ const InputFieldsTab = ({
|
||||
isRunning={actions.isRunning}
|
||||
onUploadFile={actions.handleUploadFile}
|
||||
onClearUploadedFile={actions.handleClearUploadedFile}
|
||||
onDownloadTemplate={actions.handleDownloadTemplate}
|
||||
onRun={actions.handleRun}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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 (
|
||||
<div>
|
||||
@ -24,7 +30,7 @@ const InputFieldsRequirements = ({
|
||||
)}
|
||||
{!isLoading && inputFields.length === 0 && (
|
||||
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
|
||||
{t('batch.noInputFields')}
|
||||
{emptyDescription}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && inputFields.map(field => (
|
||||
|
||||
@ -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<string, unknown>) => {
|
||||
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, string> = {
|
||||
[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) => {
|
||||
|
||||
@ -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')}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-close-line h-4 w-4" />
|
||||
<span aria-hidden="true" className="i-ri-delete-bin-line h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,15 +137,15 @@ const UploadRunPopover = ({
|
||||
{t('batch.uploadDropzoneSuffix')}
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{t('batch.uploadDropzoneDownloadPrefix')}
|
||||
{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-accent hover:underline"
|
||||
onClick={onDownloadTemplate}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{t('batch.uploadDropzoneDownloadLink')}
|
||||
{t('batch.uploadDropzoneUploadButton')}
|
||||
</button>
|
||||
{' '}
|
||||
{t('batch.uploadHint')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 (
|
||||
<Select key={selectKey}>
|
||||
<SelectTrigger
|
||||
aria-label={t('conditions.addCondition')}
|
||||
className={cn(
|
||||
'inline-flex w-auto min-w-0 border-none bg-transparent px-0 py-0 text-text-accent shadow-none hover:bg-transparent focus-visible:bg-transparent',
|
||||
disabled && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<Button
|
||||
variant="ghost-accent"
|
||||
aria-label={t('conditions.addCondition')}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('conditions.addCondition')}
|
||||
</Button>
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
popupClassName="w-[320px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border p-0 shadow-[0px_12px_16px_-4px_rgba(9,9,11,0.08),0px_4px_6px_-2px_rgba(9,9,11,0.03)]"
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-add-line h-4 w-4" />
|
||||
{t('conditions.addCondition')}
|
||||
</SelectTrigger>
|
||||
<SelectContent placement="bottom-start" popupClassName="w-[320px]">
|
||||
{metricOptionGroups.map(group => (
|
||||
<SelectGroup key={group.label}>
|
||||
<SelectLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</SelectLabel>
|
||||
{group.options.map(option => (
|
||||
<SelectItem
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
className="h-auto gap-0 px-3 py-2"
|
||||
onClick={() => {
|
||||
addCondition(resourceType, resourceId, option.variableSelector)
|
||||
setSelectKey(current => current + 1)
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<span className="truncate system-sm-medium text-text-secondary">{option.itemLabel}</span>
|
||||
<div className="max-h-[360px] overflow-y-auto bg-components-panel-bg p-1" role="menu">
|
||||
{metricOptionGroups.map(group => (
|
||||
<div key={group.label} role="group" aria-label={group.label}>
|
||||
<div className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</div>
|
||||
{group.options.map(option => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="flex h-auto w-full items-center gap-3 overflow-hidden rounded-lg px-3 py-2 text-left hover:bg-components-panel-on-panel-item-bg-hover"
|
||||
onClick={() => {
|
||||
addCondition(resourceType, resourceId, option.variableSelector)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">{option.itemLabel}</span>
|
||||
<span className="ml-auto shrink-0 system-xs-medium text-text-tertiary">
|
||||
{t(getConditionMetricValueTypeTranslationKey(option.valueType))}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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 <span className="px-1 system-sm-regular text-components-input-text-placeholder">{placeholder}</span>
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2 px-1">
|
||||
<div className="inline-flex h-6 min-w-0 items-center gap-1 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark pr-1.5 pl-[5px] shadow-xs">
|
||||
<span className={cn(getMetricValueTypeIconClassName(metric.valueType), 'h-3 w-3 shrink-0 text-text-secondary')} />
|
||||
<span className="truncate system-xs-medium text-text-secondary">{metric.itemLabel}</span>
|
||||
if (metric.kind === 'builtin' && metric.nodeInfo) {
|
||||
return (
|
||||
<div className="flex min-w-0 items-center px-1">
|
||||
<div className="inline-flex h-6 min-w-0 items-center gap-0.5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark py-1 pr-1.5 pl-[5px] shadow-xs">
|
||||
<span className="truncate system-xs-medium text-text-secondary">{getMetricVariableLabel(metric.variableSelector[1])}</span>
|
||||
<span className="system-xs-regular text-divider-deep">/</span>
|
||||
<span className="flex min-w-0 shrink-0 items-center gap-0.5">
|
||||
<BlockIcon type={getEvaluationNodeBlockType(metric.nodeInfo)} size="xs" className="size-3 rounded-[5px]" />
|
||||
<span className="max-w-[96px] truncate system-xs-medium text-text-secondary">{metric.itemLabel}</span>
|
||||
</span>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.valueType}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center px-1">
|
||||
<div className="inline-flex h-6 min-w-0 items-center gap-0.5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark py-1 pr-1.5 pl-[5px] shadow-xs">
|
||||
<span className="truncate system-xs-medium text-text-secondary">{metric.itemLabel}</span>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.valueType}</span>
|
||||
</div>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.groupLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -114,7 +125,6 @@ const ConditionMetricSelect = ({
|
||||
{group.options.map(option => (
|
||||
<SelectItem key={option.id} value={serializeVariableSelector(option.variableSelector)}>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className={cn(getMetricValueTypeIconClassName(option.valueType), 'h-3.5 w-3.5 shrink-0 text-text-tertiary')} />
|
||||
<span className="truncate">{option.itemLabel}</span>
|
||||
<span className="ml-auto shrink-0 system-xs-medium text-text-quaternary">
|
||||
{t(getConditionMetricValueTypeTranslationKey(option.valueType))}
|
||||
@ -141,7 +151,7 @@ const ConditionOperatorSelect = ({
|
||||
<SelectTrigger className="h-8 w-auto min-w-[88px] gap-1 rounded-md bg-transparent px-1.5 py-0 hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt">
|
||||
<span className="truncate system-xs-medium text-text-secondary">{getComparisonOperatorLabel(operator, t)}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[1002]" popupClassName="w-[240px] bg-components-panel-bg-blur backdrop-blur-[10px]">
|
||||
<SelectContent className="z-1002" popupClassName="w-[240px] bg-components-panel-bg-blur backdrop-blur-[10px]">
|
||||
{operators.map(nextOperator => (
|
||||
<SelectItem key={nextOperator} value={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 (
|
||||
<div className="rounded-2xl border border-divider-subtle bg-components-card-bg p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex rounded-lg border border-divider-subtle bg-background-default-subtle p-1">
|
||||
{(['and', 'or'] as const).map(operator => (
|
||||
<button
|
||||
key={operator}
|
||||
type="button"
|
||||
className={cn(
|
||||
'rounded-md px-3 py-1.5 system-xs-medium-uppercase',
|
||||
resource.judgmentConfig.logicalOperator === operator
|
||||
? 'bg-components-card-bg text-text-primary shadow-xs'
|
||||
: 'text-text-tertiary',
|
||||
)}
|
||||
onClick={() => setConditionLogicalOperator(resourceType, resourceId, operator)}
|
||||
>
|
||||
{logicalLabels[operator]}
|
||||
</button>
|
||||
))}
|
||||
<div className={cn('relative', hasMultipleConditions && 'pl-[48px]')}>
|
||||
{hasMultipleConditions && (
|
||||
<div className="absolute top-0 bottom-0 left-0 w-[48px]">
|
||||
<div className="absolute top-4 bottom-4 left-[34px] w-2.5 rounded-l-[8px] border border-r-0 border-divider-deep" />
|
||||
<div className="absolute top-1/2 right-0 h-[29px] w-4 -translate-y-1/2 bg-components-card-bg" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label={logicalLabels[logicalOperator]}
|
||||
className="absolute top-1/2 right-1 flex h-[21px] -translate-y-1/2 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-1 text-[10px] font-semibold text-text-accent-secondary shadow-xs select-none"
|
||||
onClick={toggleLogicalOperator}
|
||||
>
|
||||
{logicalLabels[logicalOperator]}
|
||||
<span aria-hidden="true" className="ml-0.5 i-ri-loop-left-line h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{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)
|
||||
<div className="space-y-3">
|
||||
{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 (
|
||||
<div key={condition.id} className="flex items-start overflow-hidden rounded-lg">
|
||||
<div className="min-w-0 flex-1 rounded-lg bg-components-input-bg-normal">
|
||||
<div className="flex items-center gap-0 pr-1">
|
||||
<div className="min-w-0 flex-1 py-1">
|
||||
<ConditionMetricSelect
|
||||
metric={metric}
|
||||
metricOptions={metricOptions}
|
||||
placeholder={t('conditions.fieldPlaceholder')}
|
||||
onChange={value => updateConditionMetric(resourceType, resourceId, condition.id, value)}
|
||||
return (
|
||||
<div key={condition.id} className="flex items-start overflow-hidden rounded-lg">
|
||||
<div className="min-w-0 flex-1 rounded-lg bg-components-input-bg-normal">
|
||||
<div className="flex items-center gap-0 pr-1">
|
||||
<div className="min-w-0 flex-1 py-1">
|
||||
<ConditionMetricSelect
|
||||
metric={metric}
|
||||
metricOptions={metricOptions}
|
||||
placeholder={t('conditions.fieldPlaceholder')}
|
||||
onChange={value => updateConditionMetric(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-3 w-px bg-divider-regular" />
|
||||
<ConditionOperatorSelect
|
||||
operator={condition.comparisonOperator}
|
||||
operators={allowedOperators}
|
||||
onChange={value => updateConditionOperator(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-3 w-px bg-divider-regular" />
|
||||
<ConditionOperatorSelect
|
||||
operator={condition.comparisonOperator}
|
||||
operators={allowedOperators}
|
||||
onChange={value => updateConditionOperator(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
{showValue && (
|
||||
<div className="border-t border-divider-subtle">
|
||||
<ConditionValueInput
|
||||
metric={metric}
|
||||
condition={condition}
|
||||
onChange={value => updateConditionValue(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-1 pl-1">
|
||||
<ActionButton
|
||||
aria-label={t('conditions.removeCondition')}
|
||||
onClick={() => removeCondition(resourceType, resourceId, condition.id)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-delete-bin-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{showValue && (
|
||||
<div className="border-t border-divider-subtle">
|
||||
<ConditionValueInput
|
||||
metric={metric}
|
||||
condition={condition}
|
||||
onChange={value => updateConditionValue(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-1 pl-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
aria-label={t('conditions.removeCondition')}
|
||||
onClick={() => removeCondition(resourceType, resourceId, condition.id)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-close-line h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 (
|
||||
<div className="group overflow-hidden rounded-xl border border-components-panel-border hover:bg-background-section">
|
||||
@ -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)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-custom-vender-solid-general-x-circle h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@ -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 = ({
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
triggerStyle === 'text'
|
||||
? (
|
||||
<button type="button" className={cn('inline-flex items-center system-sm-medium text-text-accent', triggerClassName)}>
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('metrics.add')}
|
||||
</button>
|
||||
)
|
||||
: (
|
||||
<Button variant="ghost-accent" className={triggerClassName}>
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('metrics.add')}
|
||||
</Button>
|
||||
)
|
||||
<Button variant="ghost-accent" className={triggerClassName}>
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('metrics.add')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent popupClassName="w-[360px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border p-0 shadow-[0px_12px_16px_-4px_rgba(9,9,11,0.08),0px_4px_6px_-2px_rgba(9,9,11,0.03)]">
|
||||
|
||||
@ -3,7 +3,6 @@ import type { NodeInfo } from '@/types/evaluation'
|
||||
|
||||
export type MetricSelectorProps = NonPipelineEvaluationResourceProps & {
|
||||
triggerClassName?: string
|
||||
triggerStyle?: 'button' | 'text'
|
||||
}
|
||||
|
||||
export type MetricVisualTone = 'indigo' | 'green'
|
||||
|
||||
@ -58,7 +58,6 @@ const PipelineBatchActions = ({
|
||||
isRunning={actions.isRunning}
|
||||
onUploadFile={actions.handleUploadFile}
|
||||
onClearUploadedFile={actions.handleClearUploadedFile}
|
||||
onDownloadTemplate={actions.handleDownloadTemplate}
|
||||
onRun={actions.handleRun}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "运行批量测试前,请先完成判定模型、指标和自定义映射配置。",
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user