Feat: conversation variable & variable assigner node (#7222)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
This commit is contained in:
KVOJJJin
2024-08-13 14:44:10 +08:00
committed by GitHub
parent 8b55bd5828
commit 935e72d449
128 changed files with 3354 additions and 683 deletions

View File

@@ -0,0 +1,46 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<AssignerNodeType> = {
defaultValue: {
assigned_variable_selector: [],
write_mode: WriteMode.Overwrite,
input_variable_selector: [],
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: AssignerNodeType, t: any) {
let errorMessages = ''
const {
assigned_variable_selector: assignedVarSelector,
write_mode: writeMode,
input_variable_selector: toAssignerVarSelector,
} = payload
if (!errorMessages && !assignedVarSelector?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.assignedVariable') })
if (!errorMessages && writeMode !== WriteMode.Clear) {
if (!toAssignerVarSelector?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.variable') })
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,47 @@
import type { FC } from 'react'
import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type AssignerNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.assigner'
const NodeComponent: FC<NodeProps<AssignerNodeType>> = ({
data,
}) => {
const { t } = useTranslation()
const nodes: Node[] = useNodes()
const { assigned_variable_selector: variable, write_mode: writeMode } = data
if (!variable || variable.length === 0)
return null
const isSystem = isSystemVar(variable)
const isEnv = isENV(variable)
const isChatVar = isConversationVar(variable)
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
return (
<div className='relative px-3'>
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.assignedVariable`)}</div>
<NodeVariableItem
node={node as Node}
isEnv={isEnv}
isChatVar={isChatVar}
varName={varName}
className='bg-workflow-block-parma-bg'
/>
<div className='my-2 flex justify-between items-center h-[22px] px-[5px] bg-workflow-block-parma-bg radius-sm'>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.writeMode`)}</div>
<div className='system-xs-medium text-text-secondary'>{t(`${i18nPrefix}.${writeMode}`)}</div>
</div>
</div>
)
}
export default React.memo(NodeComponent)

View File

@@ -0,0 +1,87 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import OptionCard from '../_base/components/option-card'
import useConfig from './use-config'
import { WriteMode } from './types'
import type { AssignerNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { type NodePanelProps } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.assigner'
const Panel: FC<NodePanelProps<AssignerNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleAssignedVarChanges,
isSupportAppend,
writeModeTypes,
handleWriteModeChange,
filterAssignedVar,
filterToAssignedVar,
handleToAssignedVarChange,
toAssignedVarType,
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.assignedVariable`)}
>
<VarReferencePicker
readonly={readOnly}
nodeId={id}
isShowNodeName
value={inputs.assigned_variable_selector || []}
onChange={handleAssignedVarChanges}
filterVar={filterAssignedVar}
/>
</Field>
<Field
title={t(`${i18nPrefix}.writeMode`)}
tooltip={t(`${i18nPrefix}.writeModeTip`)!}
>
<div className={cn('grid gap-2 grid-cols-3')}>
{writeModeTypes.map(type => (
<OptionCard
key={type}
title={t(`${i18nPrefix}.${type}`)}
onSelect={handleWriteModeChange(type)}
selected={inputs.write_mode === type}
disabled={!isSupportAppend && type === WriteMode.Append}
/>
))}
</div>
</Field>
{inputs.write_mode !== WriteMode.Clear && (
<Field
title={t(`${i18nPrefix}.setVariable`)}
>
<VarReferencePicker
readonly={readOnly}
nodeId={id}
isShowNodeName
value={inputs.input_variable_selector || []}
onChange={handleToAssignedVarChange}
filterVar={filterToAssignedVar}
valueTypePlaceHolder={toAssignedVarType}
/>
</Field>
)}
</div>
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,13 @@
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
export enum WriteMode {
Overwrite = 'over-write',
Append = 'append',
Clear = 'clear',
}
export type AssignerNodeType = CommonNodeType & {
assigned_variable_selector: ValueSelector
write_mode: WriteMode
input_variable_selector: ValueSelector
}

View File

@@ -0,0 +1,144 @@
import { useCallback, useMemo } from 'react'
import produce from 'immer'
import { useStoreApi } from 'reactflow'
import { isEqual } from 'lodash-es'
import { VarType } from '../../types'
import type { ValueSelector, Var } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
useIsChatMode,
useNodesReadOnly,
useWorkflow,
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
const useConfig = (id: string, payload: AssignerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const {
getNodes,
} = store.getState()
const currentNode = getNodes().find(n => n.id === id)
const isInIteration = payload.isInIteration
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
const availableNodes = useMemo(() => {
return getBeforeNodesInSameBranch(id)
}, [getBeforeNodesInSameBranch, id])
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
const { getCurrentVariableType } = useWorkflowVariables()
const assignedVarType = getCurrentVariableType({
parentNode: iterationNode,
valueSelector: inputs.assigned_variable_selector || [],
availableNodes,
isChatMode,
isConstant: false,
})
const isSupportAppend = useCallback((varType: VarType) => {
return [VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varType)
}, [])
const isCurrSupportAppend = useMemo(() => isSupportAppend(assignedVarType), [assignedVarType, isSupportAppend])
const handleAssignedVarChanges = useCallback((variable: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.assigned_variable_selector = variable as ValueSelector
draft.input_variable_selector = []
const newVarType = getCurrentVariableType({
parentNode: iterationNode,
valueSelector: draft.assigned_variable_selector || [],
availableNodes,
isChatMode,
isConstant: false,
})
if (inputs.write_mode === WriteMode.Append && !isSupportAppend(newVarType))
draft.write_mode = WriteMode.Overwrite
})
setInputs(newInputs)
}, [inputs, setInputs, getCurrentVariableType, iterationNode, availableNodes, isChatMode, isSupportAppend])
const writeModeTypes = [WriteMode.Overwrite, WriteMode.Append, WriteMode.Clear]
const handleWriteModeChange = useCallback((writeMode: WriteMode) => {
return () => {
const newInputs = produce(inputs, (draft) => {
draft.write_mode = writeMode
if (inputs.write_mode === WriteMode.Clear)
draft.input_variable_selector = []
})
setInputs(newInputs)
}
}, [inputs, setInputs])
const toAssignedVarType = useMemo(() => {
const { write_mode } = inputs
if (write_mode === WriteMode.Overwrite)
return assignedVarType
if (write_mode === WriteMode.Append) {
if (assignedVarType === VarType.arrayString)
return VarType.string
if (assignedVarType === VarType.arrayNumber)
return VarType.number
if (assignedVarType === VarType.arrayObject)
return VarType.object
}
return VarType.string
}, [assignedVarType, inputs])
const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
return selector.join('.').startsWith('conversation')
}, [])
const filterToAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
if (isEqual(selector, inputs.assigned_variable_selector))
return false
if (inputs.write_mode === WriteMode.Overwrite) {
return varPayload.type === assignedVarType
}
else if (inputs.write_mode === WriteMode.Append) {
switch (assignedVarType) {
case VarType.arrayString:
return varPayload.type === VarType.string
case VarType.arrayNumber:
return varPayload.type === VarType.number
case VarType.arrayObject:
return varPayload.type === VarType.object
default:
return false
}
}
return true
}, [inputs.assigned_variable_selector, inputs.write_mode, assignedVarType])
const handleToAssignedVarChange = useCallback((value: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.input_variable_selector = value as ValueSelector
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
readOnly,
inputs,
handleAssignedVarChanges,
assignedVarType,
isSupportAppend: isCurrSupportAppend,
writeModeTypes,
handleWriteModeChange,
filterAssignedVar,
filterToAssignedVar,
handleToAssignedVarChange,
toAssignedVarType,
}
}
export default useConfig

View File

@@ -0,0 +1,5 @@
import type { AssignerNodeType } from './types'
export const checkNodeValid = (payload: AssignerNodeType) => {
return true
}