refactor: input fields

This commit is contained in:
JzoNg 2026-04-10 12:04:51 +08:00
parent acef9630d5
commit 2df79c0404
8 changed files with 529 additions and 205 deletions

View File

@ -1,58 +1,17 @@
import type { ChangeEvent } from 'react'
import type { EvaluationResourceProps } from '../../types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { InputVar, Node } from '@/app/components/workflow/types'
import { useMutation } from '@tanstack/react-query'
import { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { toast } from '@/app/components/base/ui/toast'
import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
import { upload } from '@/service/base'
import { useStartEvaluationRunMutation } from '@/service/use-evaluation'
import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows'
import { useAppWorkflow } from '@/service/use-workflow'
import { getEvaluationMockConfig } from '../../mock'
import { useEvaluationResource, useEvaluationStore } from '../../store'
import { buildEvaluationRunRequest } from '../../store-utils'
import InputFieldsRequirements from './input-fields/input-fields-requirements'
import UploadRunPopover from './input-fields/upload-run-popover'
import { useInputFieldsActions } from './input-fields/use-input-fields-actions'
import { usePublishedInputFields } from './input-fields/use-published-input-fields'
type InputFieldsTabProps = EvaluationResourceProps & {
isPanelReady: boolean
isRunnable: boolean
}
type InputField = {
name: string
type: string
}
const getGraphNodes = (graph?: Record<string, unknown>) => {
return Array.isArray(graph?.nodes) ? graph.nodes as Node[] : []
}
const getStartNodeInputFields = (nodes?: Node[]): InputField[] => {
const startNode = nodes?.find(node => node.data.type === BlockEnum.Start) as Node<StartNodeType> | undefined
const variables = startNode?.data.variables
if (!Array.isArray(variables))
return []
return variables
.filter((variable): variable is InputVar => typeof variable.variable === 'string' && !!variable.variable)
.map(variable => ({
name: variable.variable,
type: inputVarTypeToVarType(variable.type ?? InputVarType.textInput),
}))
}
const escapeCsvCell = (value: string) => {
if (!/[",\n\r]/.test(value))
return value
return `"${value.replace(/"/g, '""')}"`
}
const InputFieldsTab = ({
resourceType,
resourceId,
@ -61,181 +20,50 @@ const InputFieldsTab = ({
}: InputFieldsTabProps) => {
const { t } = useTranslation('evaluation')
const config = getEvaluationMockConfig(resourceType)
const { data: currentAppWorkflow, isLoading: isAppWorkflowLoading } = useAppWorkflow(resourceType === 'apps' ? resourceId : '')
const { data: currentSnippetWorkflow, isLoading: isSnippetWorkflowLoading } = useSnippetPublishedWorkflow(resourceType === 'snippets' ? resourceId : '')
const inputFields = useMemo(() => {
if (resourceType === 'apps')
return getStartNodeInputFields(currentAppWorkflow?.graph.nodes)
if (resourceType === 'snippets')
return getStartNodeInputFields(getGraphNodes(currentSnippetWorkflow?.graph))
return []
}, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.graph, resourceType])
const resource = useEvaluationResource(resourceType, resourceId)
const uploadedFileId = resource.uploadedFileId
const uploadedFileName = resource.uploadedFileName
const setBatchTab = useEvaluationStore(state => state.setBatchTab)
const setUploadedFile = useEvaluationStore(state => state.setUploadedFile)
const setUploadedFileName = useEvaluationStore(state => state.setUploadedFileName)
const startRunMutation = useStartEvaluationRunMutation()
const uploadMutation = useMutation({
mutationFn: (file: File) => {
const formData = new FormData()
formData.append('file', file)
return upload({
xhr: new XMLHttpRequest(),
data: formData,
})
},
onSuccess: (uploadedFile) => {
setUploadedFile(resourceType, resourceId, {
id: uploadedFile.id,
name: typeof uploadedFile.name === 'string' ? uploadedFile.name : uploadedFileName ?? uploadedFile.id,
})
},
onError: () => {
setUploadedFile(resourceType, resourceId, null)
toast.error(t('batch.uploadError'))
},
const { inputFields, isInputFieldsLoading } = usePublishedInputFields(resourceType, resourceId)
const actions = useInputFieldsActions({
resourceType,
resourceId,
inputFields,
isInputFieldsLoading,
isPanelReady,
isRunnable,
templateFileName: config.templateFileName,
})
const fileInputRef = useRef<HTMLInputElement>(null)
const isFileUploading = uploadMutation.isPending
const isRunning = startRunMutation.isPending
const isInputFieldsLoading = (resourceType === 'apps' && isAppWorkflowLoading)
|| (resourceType === 'snippets' && isSnippetWorkflowLoading)
const canDownloadTemplate = isPanelReady && !isInputFieldsLoading && inputFields.length > 0
const isRunDisabled = !isRunnable || !uploadedFileId || isFileUploading || isRunning
const handleDownloadTemplate = () => {
if (!inputFields.length) {
toast.warning(t('batch.noInputFields'))
return
}
const content = `${inputFields.map(field => escapeCsvCell(field.name)).join(',')}\n`
const link = document.createElement('a')
link.href = `data:text/csv;charset=utf-8,${encodeURIComponent(content)}`
link.download = config.templateFileName
link.click()
}
const handleRun = () => {
if (!isRunnable) {
toast.warning(t('batch.validation'))
return
}
if (isFileUploading) {
toast.warning(t('batch.uploading'))
return
}
if (!uploadedFileId) {
toast.warning(t('batch.fileRequired'))
return
}
const body = buildEvaluationRunRequest(resource, uploadedFileId)
if (!body) {
toast.warning(t('batch.validation'))
return
}
startRunMutation.mutate({
params: {
targetType: resourceType,
targetId: resourceId,
},
body,
}, {
onSuccess: () => {
toast.success(t('batch.runStarted'))
setBatchTab(resourceType, resourceId, 'history')
},
onError: () => {
toast.error(t('batch.runFailed'))
},
})
}
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
event.target.value = ''
if (!file) {
setUploadedFile(resourceType, resourceId, null)
return
}
setUploadedFileName(resourceType, resourceId, file.name)
uploadMutation.mutate(file)
}
return (
<div className="space-y-5">
<div>
<div className="system-md-semibold text-text-primary">{t('batch.requirementsTitle')}</div>
<div className="mt-1 system-xs-regular text-text-tertiary">{t('batch.requirementsDescription')}</div>
<div className="mt-3 rounded-xl bg-background-section p-3">
{isInputFieldsLoading && (
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
{t('batch.loadingInputFields')}
</div>
)}
{!isInputFieldsLoading && inputFields.length === 0 && (
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
{t('batch.noInputFields')}
</div>
)}
{!isInputFieldsLoading && inputFields.map(field => (
<div key={field.name} className="flex items-center py-1">
<div className="rounded px-1 py-0.5 system-xs-medium text-text-tertiary">
{field.name}
</div>
<div className="text-[10px] leading-3 text-text-quaternary">
{field.type}
</div>
</div>
))}
</div>
</div>
<InputFieldsRequirements
inputFields={inputFields}
isLoading={isInputFieldsLoading}
/>
<div className="space-y-3">
<Button variant="secondary" className="w-full justify-center" disabled={!canDownloadTemplate} onClick={handleDownloadTemplate}>
<Button variant="secondary" className="w-full justify-center" disabled={!actions.canDownloadTemplate} onClick={actions.handleDownloadTemplate}>
<span aria-hidden="true" className="mr-1 i-ri-download-line h-4 w-4" />
{t('batch.downloadTemplate')}
</Button>
<input
ref={fileInputRef}
hidden
type="file"
accept=".csv,.xlsx"
onChange={handleFileChange}
<UploadRunPopover
open={actions.isUploadPopoverOpen}
onOpenChange={actions.setIsUploadPopoverOpen}
triggerDisabled={actions.uploadButtonDisabled}
inputFields={inputFields}
currentFileName={actions.currentFileName}
currentFileExtension={actions.currentFileExtension}
currentFileSize={actions.currentFileSize}
isFileUploading={actions.isFileUploading}
isRunDisabled={actions.isRunDisabled}
isRunning={actions.isRunning}
onUploadFile={actions.handleUploadFile}
onClearUploadedFile={actions.handleClearUploadedFile}
onDownloadTemplate={actions.handleDownloadTemplate}
onRun={actions.handleRun}
/>
{isPanelReady && (
<button
type="button"
className="flex w-full flex-col items-center justify-center rounded-xl border border-dashed border-divider-subtle bg-background-default-subtle px-4 py-6 text-center hover:border-components-button-secondary-border"
onClick={() => fileInputRef.current?.click()}
>
<span aria-hidden="true" className="i-ri-file-upload-line h-5 w-5 text-text-tertiary" />
<div className="mt-2 system-sm-semibold text-text-primary">{t('batch.uploadTitle')}</div>
<div className="mt-1 system-xs-regular text-text-tertiary">
{isFileUploading ? t('batch.uploading') : uploadedFileName ?? t('batch.uploadHint')}
</div>
</button>
)}
</div>
{!isRunnable && (
<div className="rounded-xl border border-divider-subtle bg-background-default-subtle px-3 py-2 system-xs-regular text-text-tertiary">
{t('batch.validation')}
</div>
)}
<Button className="w-full justify-center" variant="primary" disabled={isRunDisabled} loading={isRunning} onClick={handleRun}>
{t('batch.run')}
</Button>
</div>
)
}

View File

@ -0,0 +1,45 @@
import type { InputField } from './input-fields-utils'
import { useTranslation } from 'react-i18next'
type InputFieldsRequirementsProps = {
inputFields: InputField[]
isLoading: boolean
}
const InputFieldsRequirements = ({
inputFields,
isLoading,
}: InputFieldsRequirementsProps) => {
const { t } = useTranslation('evaluation')
return (
<div>
<div className="system-md-semibold text-text-primary">{t('batch.requirementsTitle')}</div>
<div className="mt-1 system-xs-regular text-text-tertiary">{t('batch.requirementsDescription')}</div>
<div className="mt-3 rounded-xl bg-background-section p-3">
{isLoading && (
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
{t('batch.loadingInputFields')}
</div>
)}
{!isLoading && inputFields.length === 0 && (
<div className="px-1 py-0.5 system-xs-regular text-text-tertiary">
{t('batch.noInputFields')}
</div>
)}
{!isLoading && inputFields.map(field => (
<div key={field.name} className="flex items-center py-1">
<div className="rounded px-1 py-0.5 system-xs-medium text-text-tertiary">
{field.name}
</div>
<div className="text-[10px] leading-3 text-text-quaternary">
{field.type}
</div>
</div>
))}
</div>
</div>
)
}
export default InputFieldsRequirements

View File

@ -0,0 +1,54 @@
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { InputVar, Node } from '@/app/components/workflow/types'
import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
export type InputField = {
name: string
type: string
}
export const getGraphNodes = (graph?: Record<string, unknown>) => {
return Array.isArray(graph?.nodes) ? graph.nodes as Node[] : []
}
export const getStartNodeInputFields = (nodes?: Node[]): InputField[] => {
const startNode = nodes?.find(node => node.data.type === BlockEnum.Start) as Node<StartNodeType> | undefined
const variables = startNode?.data.variables
if (!Array.isArray(variables))
return []
return variables
.filter((variable): variable is InputVar => typeof variable.variable === 'string' && !!variable.variable)
.map(variable => ({
name: variable.variable,
type: inputVarTypeToVarType(variable.type ?? InputVarType.textInput),
}))
}
const escapeCsvCell = (value: string) => {
if (!/[",\n\r]/.test(value))
return value
return `"${value.replace(/"/g, '""')}"`
}
export const buildTemplateCsvContent = (inputFields: InputField[]) => {
return `${inputFields.map(field => escapeCsvCell(field.name)).join(',')}\n`
}
export const getFileExtension = (fileName: string) => {
const extension = fileName.split('.').pop()
return extension && extension !== fileName ? extension.toUpperCase() : ''
}
export const getExampleValue = (field: InputField, booleanLabel: string) => {
if (field.type === 'number')
return '0.7'
if (field.type === 'boolean')
return booleanLabel
return field.name
}

View File

@ -0,0 +1,187 @@
import type { ChangeEvent, DragEvent } from 'react'
import type { InputField } from './input-fields-utils'
import { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/app/components/base/ui/popover'
import { cn } from '@/utils/classnames'
import { getExampleValue } from './input-fields-utils'
type UploadRunPopoverProps = {
open: boolean
onOpenChange: (open: boolean) => void
triggerDisabled: boolean
inputFields: InputField[]
currentFileName: string | null | undefined
currentFileExtension: string
currentFileSize: string | number
isFileUploading: boolean
isRunDisabled: boolean
isRunning: boolean
onUploadFile: (file: File | undefined) => void
onClearUploadedFile: () => void
onDownloadTemplate: () => void
onRun: () => void
}
const UploadRunPopover = ({
open,
onOpenChange,
triggerDisabled,
inputFields,
currentFileName,
currentFileExtension,
currentFileSize,
isFileUploading,
isRunDisabled,
isRunning,
onUploadFile,
onClearUploadedFile,
onDownloadTemplate,
onRun,
}: UploadRunPopoverProps) => {
const { t } = useTranslation('evaluation')
const { t: tCommon } = useTranslation('common')
const fileInputRef = useRef<HTMLInputElement>(null)
const previewFields = inputFields.slice(0, 3)
const booleanExampleValue = t('conditions.boolean.true')
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
onUploadFile(event.target.files?.[0])
event.target.value = ''
}
const handleDropFile = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault()
onUploadFile(event.dataTransfer.files?.[0])
}
return (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger
render={(
<Button className="w-full justify-center" variant="primary" disabled={triggerDisabled}>
{t('batch.uploadAndRun')}
</Button>
)}
/>
<PopoverContent
placement="bottom-end"
sideOffset={8}
popupClassName="w-[402px] overflow-hidden rounded-lg border border-components-panel-border p-0 shadow-[0px_20px_24px_-4px_rgba(9,9,11,0.08),0px_8px_8px_-4px_rgba(9,9,11,0.03)]"
>
<div className="flex flex-col bg-components-panel-bg">
<div className="flex flex-col gap-4 p-4">
<input
ref={fileInputRef}
hidden
type="file"
accept=".csv,.xlsx"
onChange={handleFileChange}
/>
{currentFileName
? (
<div className="flex h-20 items-center gap-3 rounded-lg border border-components-panel-border bg-components-panel-on-panel-item-bg px-3">
<div className="flex p-3">
<span aria-hidden="true" className="i-ri-file-excel-fill h-6 w-6 text-util-colors-green-green-600" />
</div>
<div className="min-w-0 flex-1 py-1 pr-2">
<div className="truncate system-xs-medium text-text-secondary">
{currentFileName}
</div>
<div className="mt-0.5 flex h-3 items-center gap-1 system-2xs-medium text-text-tertiary">
{!!currentFileExtension && <span className="uppercase">{currentFileExtension}</span>}
{!!currentFileExtension && !!currentFileSize && <span className="text-text-quaternary">·</span>}
{!!currentFileSize && <span>{currentFileSize}</span>}
</div>
</div>
<div className="flex items-center gap-1 pr-3">
{isFileUploading && (
<span aria-hidden="true" className="i-ri-loader-4-line h-4 w-4 animate-spin text-text-accent" />
)}
<button
type="button"
className="rounded-md p-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
onClick={onClearUploadedFile}
aria-label={t('batch.removeUploadedFile')}
>
<span aria-hidden="true" className="i-ri-close-line h-4 w-4" />
</button>
</div>
</div>
)
: (
<div
className="flex h-20 w-full items-center justify-center gap-3 rounded-xl border border-dashed border-components-dropzone-border bg-components-dropzone-bg p-3 text-left hover:border-components-button-secondary-border"
onDragOver={event => event.preventDefault()}
onDrop={handleDropFile}
>
<button
type="button"
className="flex shrink-0 p-3"
onClick={() => fileInputRef.current?.click()}
>
<span aria-hidden="true" className="i-ri-file-upload-line h-6 w-6 text-text-tertiary" />
<span className="sr-only">{t('batch.uploadTitle')}</span>
</button>
<div className="min-w-0 flex-1 text-left">
<div className="system-md-regular text-text-secondary">
{t('batch.uploadDropzonePrefix')}
{' '}
<span className="system-md-semibold">{t('batch.uploadDropzoneEmphasis')}</span>
{' '}
{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}
>
{t('batch.uploadDropzoneDownloadLink')}
</button>
</div>
</div>
</div>
)}
{!!previewFields.length && (
<div className="space-y-1">
<div className="system-md-semibold text-text-secondary">{t('batch.example')}</div>
<div className="flex overflow-hidden rounded-lg border border-divider-regular">
{previewFields.map((field, index) => (
<div key={field.name} className={cn('min-w-0 flex-1', index < previewFields.length - 1 && 'border-r border-divider-subtle')}>
<div className="min-h-8 border-b border-divider-regular px-3 py-2 system-xs-medium-uppercase text-text-tertiary">
{field.name}
</div>
<div className="min-h-8 px-3 py-2 system-sm-regular text-text-secondary">
{getExampleValue(field, booleanExampleValue)}
</div>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex items-end justify-end gap-2 border-t border-components-panel-border px-4 py-4">
<Button variant="secondary" className="rounded-lg" onClick={() => onOpenChange(false)}>
{tCommon('operation.cancel')}
</Button>
<Button className="flex-1 justify-center rounded-lg" variant="primary" disabled={isRunDisabled} loading={isRunning} onClick={onRun}>
<span aria-hidden="true" className="mr-1 i-ri-play-fill h-5 w-5" />
{t('batch.run')}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
)
}
export default UploadRunPopover

View File

@ -0,0 +1,165 @@
import type { EvaluationResourceProps } from '../../../types'
import type { InputField } from './input-fields-utils'
import { useMutation } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from '@/app/components/base/ui/toast'
import { upload } from '@/service/base'
import { useStartEvaluationRunMutation } from '@/service/use-evaluation'
import { formatFileSize } from '@/utils/format'
import { useEvaluationResource, useEvaluationStore } from '../../../store'
import { buildEvaluationRunRequest } from '../../../store-utils'
import { buildTemplateCsvContent, getFileExtension } from './input-fields-utils'
type UploadedFileMeta = {
name: string
size: number
}
type UseInputFieldsActionsParams = EvaluationResourceProps & {
inputFields: InputField[]
isInputFieldsLoading: boolean
isPanelReady: boolean
isRunnable: boolean
templateFileName: string
}
export const useInputFieldsActions = ({
resourceType,
resourceId,
inputFields,
isInputFieldsLoading,
isPanelReady,
isRunnable,
templateFileName,
}: UseInputFieldsActionsParams) => {
const { t } = useTranslation('evaluation')
const resource = useEvaluationResource(resourceType, resourceId)
const setBatchTab = useEvaluationStore(state => state.setBatchTab)
const setUploadedFile = useEvaluationStore(state => state.setUploadedFile)
const setUploadedFileName = useEvaluationStore(state => state.setUploadedFileName)
const startRunMutation = useStartEvaluationRunMutation()
const [isUploadPopoverOpen, setIsUploadPopoverOpen] = useState(false)
const [uploadedFileMeta, setUploadedFileMeta] = useState<UploadedFileMeta | null>(null)
const uploadMutation = useMutation({
mutationFn: (file: File) => {
const formData = new FormData()
formData.append('file', file)
return upload({
xhr: new XMLHttpRequest(),
data: formData,
})
},
onSuccess: (uploadedFile, file) => {
setUploadedFile(resourceType, resourceId, {
id: uploadedFile.id,
name: typeof uploadedFile.name === 'string' ? uploadedFile.name : file.name,
})
},
onError: () => {
setUploadedFileMeta(null)
setUploadedFile(resourceType, resourceId, null)
toast.error(t('batch.uploadError'))
},
})
const isFileUploading = uploadMutation.isPending
const isRunning = startRunMutation.isPending
const uploadedFileId = resource.uploadedFileId
const currentFileName = uploadedFileMeta?.name ?? resource.uploadedFileName
const canDownloadTemplate = isPanelReady && !isInputFieldsLoading && inputFields.length > 0
const isRunDisabled = !isRunnable || !uploadedFileId || isFileUploading || isRunning
const uploadButtonDisabled = !isPanelReady || isInputFieldsLoading || isRunning
const handleDownloadTemplate = () => {
if (!inputFields.length) {
toast.warning(t('batch.noInputFields'))
return
}
const content = buildTemplateCsvContent(inputFields)
const link = document.createElement('a')
link.href = `data:text/csv;charset=utf-8,${encodeURIComponent(content)}`
link.download = templateFileName
link.click()
}
const handleRun = () => {
if (!isRunnable) {
toast.warning(t('batch.validation'))
return
}
if (isFileUploading) {
toast.warning(t('batch.uploading'))
return
}
if (!uploadedFileId) {
toast.warning(t('batch.fileRequired'))
return
}
const body = buildEvaluationRunRequest(resource, uploadedFileId)
if (!body) {
toast.warning(t('batch.validation'))
return
}
startRunMutation.mutate({
params: {
targetType: resourceType,
targetId: resourceId,
},
body,
}, {
onSuccess: () => {
toast.success(t('batch.runStarted'))
setIsUploadPopoverOpen(false)
setBatchTab(resourceType, resourceId, 'history')
},
onError: () => {
toast.error(t('batch.runFailed'))
},
})
}
const handleUploadFile = (file: File | undefined) => {
if (!file) {
setUploadedFileMeta(null)
setUploadedFile(resourceType, resourceId, null)
return
}
setUploadedFileMeta({
name: file.name,
size: file.size,
})
setUploadedFileName(resourceType, resourceId, file.name)
uploadMutation.mutate(file)
}
const handleClearUploadedFile = () => {
setUploadedFileMeta(null)
setUploadedFile(resourceType, resourceId, null)
}
return {
canDownloadTemplate,
currentFileExtension: currentFileName ? getFileExtension(currentFileName) : '',
currentFileName,
currentFileSize: uploadedFileMeta ? formatFileSize(uploadedFileMeta.size) : '',
handleClearUploadedFile,
handleDownloadTemplate,
handleRun,
handleUploadFile,
isFileUploading,
isRunning,
isRunDisabled,
isUploadPopoverOpen,
setIsUploadPopoverOpen,
uploadButtonDisabled,
}
}

View File

@ -0,0 +1,29 @@
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'
export const usePublishedInputFields = (
resourceType: EvaluationResourceType,
resourceId: string,
) => {
const { data: currentAppWorkflow, isLoading: isAppWorkflowLoading } = useAppWorkflow(resourceType === 'apps' ? resourceId : '')
const { data: currentSnippetWorkflow, isLoading: isSnippetWorkflowLoading } = useSnippetPublishedWorkflow(resourceType === 'snippets' ? resourceId : '')
const inputFields = useMemo(() => {
if (resourceType === 'apps')
return getStartNodeInputFields(currentAppWorkflow?.graph.nodes)
if (resourceType === 'snippets')
return getStartNodeInputFields(getGraphNodes(currentSnippetWorkflow?.graph))
return []
}, [currentAppWorkflow?.graph.nodes, currentSnippetWorkflow?.graph, resourceType])
return {
inputFields,
isInputFieldsLoading: (resourceType === 'apps' && isAppWorkflowLoading)
|| (resourceType === 'snippets' && isSnippetWorkflowLoading),
}
}

View File

@ -2,11 +2,13 @@
"batch.description": "Execute batch evaluations and track performance history.",
"batch.downloadTemplate": "Download Excel 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.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",
"batch.requirementsDescription": "The input variables required to run this batch test. Ensure your uploaded dataset matches these fields.",
"batch.requirementsTitle": "Data requirements",
"batch.run": "Run Test",
@ -18,6 +20,12 @@
"batch.tabs.history": "Test History",
"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.uploadError": "Failed to upload file.",
"batch.uploadHint": "Select a .csv or .xlsx file",
"batch.uploadTitle": "Upload test file",

View File

@ -2,11 +2,13 @@
"batch.description": "执行批量评测并追踪性能历史。",
"batch.downloadTemplate": "下载 Excel 模板",
"batch.emptyHistory": "还没有测试历史。",
"batch.example": "示例:",
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
"batch.loadingInputFields": "正在加载输入字段...",
"batch.noInputFields": "未找到已发布 Start 节点的输入字段。",
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
"batch.noticeTitle": "快速开始",
"batch.removeUploadedFile": "移除已上传文件",
"batch.requirementsDescription": "运行此批量测试所需的输入变量。请确保上传的数据集包含这些字段。",
"batch.requirementsTitle": "数据要求",
"batch.run": "运行测试",
@ -18,6 +20,12 @@
"batch.tabs.history": "测试历史",
"batch.tabs.input-fields": "输入字段",
"batch.title": "批量测试",
"batch.uploadAndRun": "上传并运行测试",
"batch.uploadDropzoneDownloadLink": "下载",
"batch.uploadDropzoneDownloadPrefix": "还没有模板?",
"batch.uploadDropzoneEmphasis": "已填写的",
"batch.uploadDropzonePrefix": "拖拽你的",
"batch.uploadDropzoneSuffix": "Excel 模板",
"batch.uploadError": "文件上传失败。",
"batch.uploadHint": "选择 .csv 或 .xlsx 文件",
"batch.uploadTitle": "上传测试文件",