feat: node before and after run

This commit is contained in:
Joel 2024-03-15 11:26:10 +08:00
parent 86d2c1184c
commit 1b8c8b0a43
8 changed files with 130 additions and 46 deletions

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer' import produce from 'immer'
import RemoveButton from '../remove-button' import RemoveButton from '../remove-button'
import VarReferencePicker from './var-reference-picker' import VarReferencePicker from './var-reference-picker'
@ -26,6 +27,8 @@ const VarList: FC<Props> = ({
onlyLeafNodeVar, onlyLeafNodeVar,
onlyVarType, onlyVarType,
}) => { }) => {
const { t } = useTranslation()
const handleVarNameChange = useCallback((index: number) => { const handleVarNameChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => { return (e: React.ChangeEvent<HTMLInputElement>) => {
const newList = produce(list, (draft) => { const newList = produce(list, (draft) => {
@ -54,7 +57,7 @@ const VarList: FC<Props> = ({
}) })
onChange(newList) onChange(newList)
} }
}, [list, onChange]) }, [isSupportConstantValue, list, onChange])
const handleVarRemove = useCallback((index: number) => { const handleVarRemove = useCallback((index: number) => {
return () => { return () => {
@ -73,8 +76,10 @@ const VarList: FC<Props> = ({
readOnly={readonly} readOnly={readonly}
value={list[index].variable} value={list[index].variable}
onChange={handleVarNameChange(index)} onChange={handleVarNameChange(index)}
className='w-[120px] h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200' placeholder={t('workflow.common.variableNamePlaceholder')!}
type='text' /> className='w-[120px] h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
type='text'
/>
<VarReferencePicker <VarReferencePicker
nodeId={nodeId} nodeId={nodeId}
readonly={readonly} readonly={readonly}

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import { isArray } from 'lodash-es' import { isArray } from 'lodash-es'
import VarReferencePopup from './var-reference-popup' import VarReferencePopup from './var-reference-popup'
@ -57,6 +58,8 @@ const VarReferencePicker: FC<Props> = ({
onlyLeafNodeVar, onlyLeafNodeVar,
onlyVarType, onlyVarType,
}) => { }) => {
const { t } = useTranslation()
const isChatMode = useIsChatMode() const isChatMode = useIsChatMode()
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType) const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
const isConstant = isSupportConstantValue && varKindType === VarKindType.static const isConstant = isSupportConstantValue && varKindType === VarKindType.static
@ -110,7 +113,8 @@ const VarReferencePicker: FC<Props> = ({
onChange('', value) onChange('', value)
else else
onChange([], value) onChange([], value)
}, [varKindType]) }, [onChange])
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const [isFocus, setIsFocus] = useState(false) const [isFocus, setIsFocus] = useState(false)
const [controlFocus, setControlFocus] = useState(0) const [controlFocus, setControlFocus] = useState(0)
@ -166,27 +170,29 @@ const VarReferencePicker: FC<Props> = ({
) )
: ( : (
<div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}> <div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}>
{hasValue && ( {hasValue
<> ? (
{isShowNodeName && ( <>
<div className='flex items-center'> {isShowNodeName && (
<div className='p-[1px]'> <div className='flex items-center'>
<VarBlockIcon <div className='p-[1px]'>
className='!text-gray-900' <VarBlockIcon
type={outputVarNode?.type} className='!text-gray-900'
/> type={outputVarNode?.type}
/>
</div>
<div className='mx-0.5 text-xs font-medium text-gray-700'>{outputVarNode?.title}</div>
<Line3 className='mr-0.5'></Line3>
</div> </div>
<div className='mx-0.5 text-xs font-medium text-gray-700'>{outputVarNode?.title}</div> )}
<Line3 className='mr-0.5'></Line3> <div className='flex items-center text-primary-600'>
<Variable02 className='w-3.5 h-3.5' />
<div className='ml-0.5 text-xs font-medium'>{varName}</div>
</div> </div>
)} <div className='ml-0.5 text-xs font-normal text-gray-500 capitalize'>{getVarType()}</div>
<div className='flex items-center text-primary-600'> </>
<Variable02 className='w-3.5 h-3.5' /> )
<div className='ml-0.5 text-xs font-medium'>{varName}</div> : <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
</div>
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize'>{getVarType()}</div>
</>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -1,28 +1,62 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks' import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { CommonNodeType, InputVar, Variable } from '@/app/components/workflow/types' import type { CheckValidRes, CommonNodeType, InputVar, Variable } from '@/app/components/workflow/types'
import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types' import { BlockEnum, InputVarType, NodeRunningStatus } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { singleNodeRun } from '@/service/workflow' import { singleNodeRun } from '@/service/workflow'
// import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import CodeDefault from '@/app/components/workflow/nodes/code/default'
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
const { checkValid: checkCodeValid } = CodeDefault
const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.Code]: checkCodeValid,
[BlockEnum.HttpRequest]: HTTPDefault.checkValid,
} as any
type Params<T> = { type Params<T> = {
id: string id: string
data: CommonNodeType<T> data: CommonNodeType<T>
defaultRunInputData: Record<string, any> defaultRunInputData: Record<string, any>
isInvalid?: () => boolean beforeRunCheckValid?: () => CheckValidRes
} }
const useOneStepRun = <T>({ id, data, defaultRunInputData, isInvalid = () => true }: Params<T>) => { const useOneStepRun = <T>({
id,
data,
defaultRunInputData,
beforeRunCheckValid = () => ({ isValid: true }),
}: Params<T>) => {
const { t } = useTranslation() const { t } = useTranslation()
const checkValid = checkValidFns[data.type]
const appId = useAppStore.getState().appDetail?.id const appId = useAppStore.getState().appDetail?.id
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {}) const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
const [runResult, setRunResult] = useState<any>(null) const [runResult, setRunResult] = useState<any>(null)
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate() const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const isShowSingleRun = data._isSingleRun const [canShowSingleRun, setCanShowSingleRun] = useState(false)
const isShowSingleRun = data._isSingleRun && canShowSingleRun
useEffect(() => {
if (data._isSingleRun) {
const { isValid, errorMessage } = checkValid(data, t)
setCanShowSingleRun(isValid)
if (!isValid) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: errorMessage,
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data._isSingleRun])
const hideSingleRun = () => { const hideSingleRun = () => {
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
@ -35,8 +69,14 @@ const useOneStepRun = <T>({ id, data, defaultRunInputData, isInvalid = () => tru
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
const handleRun = async () => { const handleRun = async () => {
if (!isInvalid()) const { isValid, errorMessage } = beforeRunCheckValid()
return if (!isValid) {
Toast.notify({
type: 'error',
message: errorMessage!,
})
return false
}
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
data: { data: {
@ -77,10 +117,6 @@ const useOneStepRun = <T>({ id, data, defaultRunInputData, isInvalid = () => tru
_singleRunningStatus: NodeRunningStatus.Succeeded, _singleRunningStatus: NodeRunningStatus.Succeeded,
}, },
}) })
// Toast.notify({
// type: 'success',
// message: t('common.api.success'),
// })
} }
const handleStop = () => { const handleStop = () => {

View File

@ -2,6 +2,8 @@ import type { NodeDefault } from '../../types'
import { CodeLanguage, type CodeNodeType } from './types' import { CodeLanguage, type CodeNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants' import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<CodeNodeType> = { const nodeDefault: NodeDefault<CodeNodeType> = {
defaultValue: { defaultValue: {
code: '', code: '',
@ -17,18 +19,22 @@ const nodeDefault: NodeDefault<CodeNodeType> = {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes return nodes
}, },
checkValid(payload: CodeNodeType) { checkValid(payload: CodeNodeType, t: any) {
let isValid = true
let errorMessages = '' let errorMessages = ''
if (payload.type) { const { code, variables } = payload
isValid = true if (!errorMessages && variables.filter(v => !v.variable).length > 0)
errorMessages = '' errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
} if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
if (!errorMessages && !code)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.code`) })
return { return {
isValid, isValid: !errorMessages,
errorMessage: errorMessages, errorMessage: errorMessages,
} }
}, },
} }
export default nodeDefault export default nodeDefault

View File

@ -48,6 +48,12 @@ const useConfig = (id: string, payload: CodeNodeType) => {
id, id,
data: inputs, data: inputs,
defaultRunInputData: {}, defaultRunInputData: {},
beforeRunCheckValid: () => {
return {
isValid: true,
// errorMessage: 'xxxx',
}
},
}) })
const varInputs = toVarInputs(inputs.variables) const varInputs = toVarInputs(inputs.variables)
@ -63,7 +69,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
const setInputVarValues = useCallback((newPayload: Record<string, any>) => { const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload) setRunInputData(newPayload)
}, [runInputData, setRunInputData]) }, [setRunInputData])
return { return {
inputs, inputs,

View File

@ -159,7 +159,7 @@ export type NodeDefault<T> = {
defaultValue: Partial<T> defaultValue: Partial<T>
getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[] getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[]
getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[] getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[]
checkValid: (payload: T) => { isValid: boolean; errorMessage?: string } checkValid: (payload: T, t: any) => { isValid: boolean; errorMessage?: string }
} }
export type OnSelectBlock = (type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => void export type OnSelectBlock = (type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => void
@ -199,3 +199,8 @@ export type OnNodeAdd = (
nextNodeTargetHandle?: string nextNodeTargetHandle?: string
} }
) => void ) => void
export type CheckValidRes = {
isValid: boolean
errorMessage?: string
}

View File

@ -24,6 +24,16 @@ const translation = {
addTitle: 'Add title...', addTitle: 'Add title...',
addDescription: 'Add description...', addDescription: 'Add description...',
noVar: 'No variable', noVar: 'No variable',
variableNamePlaceholder: 'Variable name',
setVarValuePlaceholder: 'Set variable',
},
errorMsg: {
fieldRequired: '{{field}} is required',
fields: {
variable: 'Variable Name',
variableValue: 'Variable Value',
code: 'Code',
},
}, },
singleRun: { singleRun: {
testRun: 'Test Run ', testRun: 'Test Run ',

View File

@ -24,6 +24,16 @@ const translation = {
addTitle: '添加标题...', addTitle: '添加标题...',
addDescription: '添加描述...', addDescription: '添加描述...',
noVar: '没有变量', noVar: '没有变量',
variableNamePlaceholder: '变量名',
setVarValuePlaceholder: '设置变量值',
},
errorMsg: {
fieldRequired: '{{field}} 不能为空',
fields: {
variable: '变量名',
variableValue: '变量值',
code: '代码',
},
}, },
singleRun: { singleRun: {
testRun: '测试运行 ', testRun: '测试运行 ',