Feat/loop break node (#17268)
This commit is contained in:
@@ -14,6 +14,9 @@ const HelpLink = ({
|
||||
const { t } = useTranslation()
|
||||
const link = useNodeHelpLink(nodeType)
|
||||
|
||||
if (!link)
|
||||
return null
|
||||
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.userProfile.helpCenter')}
|
||||
|
@@ -164,7 +164,7 @@ const PanelOperatorPopup = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
showHelpLink && (
|
||||
showHelpLink && link && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<a
|
||||
|
@@ -58,7 +58,8 @@ const VariableTag = ({
|
||||
{node && (
|
||||
<>
|
||||
<VarBlockIcon
|
||||
type={BlockEnum.Start}
|
||||
type={node.data.type || BlockEnum.Start}
|
||||
className='mr-0.5'
|
||||
/>
|
||||
<div
|
||||
className='max-w-[60px] truncate font-medium text-text-secondary'
|
||||
|
@@ -3,7 +3,7 @@ import { isArray, uniq } from 'lodash-es'
|
||||
import type { CodeNodeType } from '../../../code/types'
|
||||
import type { EndNodeType } from '../../../end/types'
|
||||
import type { AnswerNodeType } from '../../../answer/types'
|
||||
import type { LLMNodeType } from '../../../llm/types'
|
||||
import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types'
|
||||
import type { IfElseNodeType } from '../../../if-else/types'
|
||||
import type { TemplateTransformNodeType } from '../../../template-transform/types'
|
||||
@@ -21,6 +21,8 @@ import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/type
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
import {
|
||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
|
||||
@@ -55,19 +57,81 @@ const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
const structTypeToVarType = (type: Type): VarType => {
|
||||
return ({
|
||||
[Type.string]: VarType.string,
|
||||
[Type.number]: VarType.number,
|
||||
[Type.boolean]: VarType.boolean,
|
||||
[Type.object]: VarType.object,
|
||||
[Type.array]: VarType.array,
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
export const varTypeToStructType = (type: VarType): Type => {
|
||||
return ({
|
||||
[VarType.string]: Type.string,
|
||||
[VarType.number]: Type.number,
|
||||
[VarType.boolean]: Type.boolean,
|
||||
[VarType.object]: Type.object,
|
||||
[VarType.array]: Type.array,
|
||||
} as any)[type] || Type.string
|
||||
}
|
||||
|
||||
const findExceptVarInStructuredProperties = (properties: Record<string, StructField>, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record<string, StructField> => {
|
||||
const res = produce(properties, (draft) => {
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const item = properties[key]
|
||||
const isObj = item.type === Type.object
|
||||
if (!isObj && !filterVar({
|
||||
variable: key,
|
||||
type: structTypeToVarType(item.type),
|
||||
}, [key])) {
|
||||
delete properties[key]
|
||||
return
|
||||
}
|
||||
if (item.type === Type.object && item.properties)
|
||||
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
|
||||
})
|
||||
return draft
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => {
|
||||
const res = produce(structuredOutput, (draft) => {
|
||||
const properties = draft.schema.properties
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const item = properties[key]
|
||||
const isObj = item.type === Type.object
|
||||
if (!isObj && !filterVar({
|
||||
variable: key,
|
||||
type: structTypeToVarType(item.type),
|
||||
}, [key])) {
|
||||
delete properties[key]
|
||||
return
|
||||
}
|
||||
if (item.type === Type.object && item.properties)
|
||||
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
|
||||
})
|
||||
return draft
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => {
|
||||
const { children } = obj
|
||||
const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties
|
||||
|
||||
const res: Var = {
|
||||
variable: obj.variable,
|
||||
type: isFile ? VarType.file : VarType.object,
|
||||
children: children.filter((item: Var) => {
|
||||
children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => {
|
||||
const { children } = item
|
||||
const currSelector = [...value_selector, item.variable]
|
||||
if (!children)
|
||||
return filterVar(item, currSelector)
|
||||
|
||||
const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children
|
||||
return obj.children && obj.children?.length > 0
|
||||
return obj.children && (obj.children as Var[])?.length > 0
|
||||
}),
|
||||
}
|
||||
return res
|
||||
@@ -139,10 +203,17 @@ const formatItem = (
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
res.vars = LLM_OUTPUT_STRUCT
|
||||
res.vars = [...LLM_OUTPUT_STRUCT]
|
||||
if (data.structured_output_enabled && data.structured_output?.schema?.properties && Object.keys(data.structured_output.schema.properties).length > 0) {
|
||||
res.vars.push({
|
||||
variable: 'structured_output',
|
||||
type: VarType.object,
|
||||
children: data.structured_output,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT
|
||||
break
|
||||
@@ -286,6 +357,21 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Loop: {
|
||||
const { loop_variables } = data as LoopNodeType
|
||||
res.isLoop = true
|
||||
res.vars = loop_variables?.map((v) => {
|
||||
return {
|
||||
variable: v.label,
|
||||
type: v.var_type,
|
||||
isLoopVariable: true,
|
||||
nodeId: res.nodeId,
|
||||
}
|
||||
}) || []
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.DocExtractor: {
|
||||
res.vars = [
|
||||
{
|
||||
@@ -405,7 +491,7 @@ const formatItem = (
|
||||
return false
|
||||
|
||||
const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile)
|
||||
return obj?.children && obj?.children.length > 0
|
||||
return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0)
|
||||
}).map((v) => {
|
||||
const isFile = v.type === VarType.file
|
||||
|
||||
@@ -457,7 +543,7 @@ export const toNodeOutputVars = (
|
||||
},
|
||||
}
|
||||
const res = [
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
|
||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
|
||||
].map((node) => {
|
||||
@@ -579,8 +665,7 @@ export const getVarType = ({
|
||||
isConstant,
|
||||
environmentVariables = [],
|
||||
conversationVariables = [],
|
||||
}:
|
||||
{
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
isIterationItem?: boolean
|
||||
@@ -644,7 +729,7 @@ export const getVarType = ({
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
return node?.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]
|
||||
@@ -655,10 +740,30 @@ export const getVarType = ({
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
|
||||
if (isSystem || isEnv || isChatVar) {
|
||||
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
const targetVar = curr.find((v: any) => v.variable === valueSelector[1])
|
||||
if (!targetVar)
|
||||
return VarType.string
|
||||
|
||||
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
|
||||
if (isStructuredOutputVar) {
|
||||
let currProperties = targetVar.children.schema;
|
||||
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 3
|
||||
if (!currProperties)
|
||||
return
|
||||
|
||||
currProperties = currProperties.properties[key]
|
||||
if (isLast)
|
||||
type = structTypeToVarType(currProperties?.type)
|
||||
})
|
||||
return type
|
||||
}
|
||||
|
||||
(valueSelector as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
if (Array.isArray(curr))
|
||||
@@ -741,6 +846,9 @@ export const toNodeAvailableVars = ({
|
||||
},
|
||||
],
|
||||
}
|
||||
const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id)
|
||||
if (iterationIndex > -1)
|
||||
beforeNodesOutputVars.splice(iterationIndex, 1)
|
||||
beforeNodesOutputVars.unshift(iterationVar)
|
||||
}
|
||||
return beforeNodesOutputVars
|
||||
@@ -1181,17 +1289,27 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
})
|
||||
return newNode
|
||||
}
|
||||
|
||||
const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
if (!v.variable)
|
||||
return
|
||||
|
||||
res.push([...parentValueSelector, v.variable])
|
||||
const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties
|
||||
|
||||
if (v.children && v.children.length > 0) {
|
||||
v.children.forEach((child) => {
|
||||
if ((v.children as Var[])?.length > 0) {
|
||||
(v.children as Var[]).forEach((child) => {
|
||||
varToValueSelectorList(child, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
if (isStructuredOutput) {
|
||||
Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => {
|
||||
varToValueSelectorList({
|
||||
variable: key,
|
||||
type: structTypeToVarType((v.children as StructuredOutput)?.schema?.properties[key].type),
|
||||
}, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
@@ -1225,7 +1343,16 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res)
|
||||
const vars = [...LLM_OUTPUT_STRUCT]
|
||||
const llmNodeData = data as LLMNodeType
|
||||
if (llmNodeData.structured_output_enabled && llmNodeData.structured_output?.schema?.properties && Object.keys(llmNodeData.structured_output.schema.properties).length > 0) {
|
||||
vars.push({
|
||||
variable: 'structured_output',
|
||||
type: VarType.object,
|
||||
children: llmNodeData.structured_output,
|
||||
})
|
||||
}
|
||||
varsToValueSelectorList(vars, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
|
@@ -101,7 +101,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const { availableNodes, availableVars } = useAvailableVarList(nodeId, {
|
||||
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar,
|
||||
passedInAvailableNodes,
|
||||
filterVar,
|
||||
|
@@ -16,6 +16,7 @@ import Input from '@/app/components/base/input'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
@@ -38,6 +39,7 @@ type ItemProps = {
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
isException?: boolean
|
||||
isLoopVar?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
@@ -50,6 +52,7 @@ const Item: FC<ItemProps> = ({
|
||||
itemWidth,
|
||||
isSupportFileVar,
|
||||
isException,
|
||||
isLoopVar,
|
||||
}) => {
|
||||
const isFile = itemData.type === VarType.file
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
|
||||
@@ -112,9 +115,10 @@ const Item: FC<ItemProps> = ({
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
|
||||
{isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />}
|
||||
{!isEnv && !isChatVar && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
|
||||
)}
|
||||
@@ -317,6 +321,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
/>
|
||||
))}
|
||||
</div>))
|
||||
|
@@ -24,11 +24,11 @@ const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
@@ -46,7 +46,7 @@ const useAvailableVarList = (nodeId: string, {
|
||||
return {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes,
|
||||
availableNodesWithParent: availableNodes,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,9 +26,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.LoopStart]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
@@ -52,9 +50,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.LoopStart]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
@@ -62,7 +58,12 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}, [language])
|
||||
}, [language]) as Record<string, string>
|
||||
|
||||
return `${prefixLink}${linkMap[nodeType]}`
|
||||
const link = linkMap[nodeType]
|
||||
|
||||
if (!link)
|
||||
return ''
|
||||
|
||||
return `${prefixLink}${link}`
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
@@ -46,7 +46,7 @@ import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type BaseNodeProps = {
|
||||
children: ReactNode
|
||||
children: ReactElement
|
||||
} & NodeProps
|
||||
|
||||
const BaseNode: FC<BaseNodeProps> = ({
|
||||
@@ -104,6 +104,30 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
|
||||
const LoopIndex = useMemo(() => {
|
||||
let text = ''
|
||||
|
||||
if (data._runningStatus === NodeRunningStatus.Running)
|
||||
text = t('workflow.nodes.loop.currentLoopCount', { count: data._loopIndex })
|
||||
if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed)
|
||||
text = t('workflow.nodes.loop.totalLoopCount', { count: data._loopIndex })
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium mr-2 text-text-tertiary',
|
||||
data._runningStatus === NodeRunningStatus.Running && 'text-text-accent',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [data._loopIndex, data._runningStatus, t])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -233,11 +257,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data._loopLength && data._loopIndex && data._runningStatus === NodeRunningStatus.Running && (
|
||||
<div className='mr-1.5 text-xs font-medium text-primary-600'>
|
||||
{data._loopIndex > data._loopLength ? data._loopLength : data._loopIndex}/{data._loopLength}
|
||||
</div>
|
||||
)
|
||||
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
|
||||
}
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
|
@@ -95,10 +95,13 @@ const VarList: FC<Props> = ({
|
||||
}, [onOpen])
|
||||
|
||||
const handleFilterToAssignedVar = useCallback((index: number) => {
|
||||
return (payload: Var, valueSelector: ValueSelector) => {
|
||||
return (payload: Var) => {
|
||||
const item = list[index]
|
||||
const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined
|
||||
|
||||
if (item.variable_selector.join('.') === `${payload.nodeId}.${payload.variable}`)
|
||||
return false
|
||||
|
||||
if (!filterToAssignedVar || !item.variable_selector || !assignedVarType || !item.operation)
|
||||
return true
|
||||
|
||||
|
@@ -13,7 +13,6 @@ const NodeComponent: FC<NodeProps<AssignerNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nodes: Node[] = useNodes()
|
||||
if (data.version === '2') {
|
||||
const { items: operationItems } = data
|
||||
|
@@ -31,7 +31,7 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
}
|
||||
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
@@ -39,11 +39,9 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
const currentNode = getNodes().find(n => n.id === id)
|
||||
const isInIteration = payload.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const isInLoop = payload.isInLoop
|
||||
const loopNode = isInLoop ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(id)
|
||||
}, [getBeforeNodesInSameBranch, id])
|
||||
return getBeforeNodesInSameBranchIncludeParent(id)
|
||||
}, [getBeforeNodesInSameBranchIncludeParent, id])
|
||||
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
|
||||
const newSetInputs = useCallback((newInputs: AssignerNodeType) => {
|
||||
const finalInputs = produce(newInputs, (draft) => {
|
||||
@@ -56,13 +54,13 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getAssignedVarType = useCallback((valueSelector: ValueSelector) => {
|
||||
return getCurrentVariableType({
|
||||
parentNode: isInIteration ? iterationNode : loopNode,
|
||||
parentNode: isInIteration ? iterationNode : null,
|
||||
valueSelector: valueSelector || [],
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
}, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode])
|
||||
}, [getCurrentVariableType, isInIteration, iterationNode, availableNodes, isChatMode])
|
||||
|
||||
const handleOperationListChanges = useCallback((items: AssignerNodeOperation[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
@@ -91,6 +89,8 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
}, [])
|
||||
|
||||
const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
|
||||
if (varPayload.isLoopVariable)
|
||||
return true
|
||||
return selector.join('.').startsWith('conversation')
|
||||
}, [])
|
||||
|
||||
|
@@ -6,7 +6,10 @@ import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
@@ -115,6 +118,7 @@ export const useNodeIterationInteractions = () => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
|
@@ -15,4 +15,51 @@ export type LLMNodeType = CommonNodeType & {
|
||||
enabled: boolean
|
||||
configs?: VisionSetting
|
||||
}
|
||||
structured_output_enabled?: boolean
|
||||
structured_output?: StructuredOutput
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
string = 'string',
|
||||
number = 'number',
|
||||
boolean = 'boolean',
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
}
|
||||
|
||||
export enum ArrayType {
|
||||
string = 'array[string]',
|
||||
number = 'array[number]',
|
||||
boolean = 'array[boolean]',
|
||||
object = 'array[object]',
|
||||
}
|
||||
|
||||
export type TypeWithArray = Type | ArrayType
|
||||
|
||||
type ArrayItemType = Exclude<Type, Type.array>
|
||||
export type ArrayItems = Omit<Field, 'type'> & { type: ArrayItemType }
|
||||
|
||||
export type SchemaEnumType = string[] | number[]
|
||||
|
||||
export type Field = {
|
||||
type: Type
|
||||
properties?: { // Object has properties
|
||||
[key: string]: Field
|
||||
}
|
||||
required?: string[] // Key of required properties in object
|
||||
description?: string
|
||||
items?: ArrayItems // Array has items. Define the item type
|
||||
enum?: SchemaEnumType // Enum values
|
||||
additionalProperties?: false // Required in object by api. Just set false
|
||||
}
|
||||
|
||||
export type StructuredOutput = {
|
||||
schema: SchemaRoot
|
||||
}
|
||||
|
||||
export type SchemaRoot = {
|
||||
type: Type.object
|
||||
properties: Record<string, Field>
|
||||
required?: string[]
|
||||
additionalProperties: false
|
||||
}
|
||||
|
23
web/app/components/workflow/nodes/loop-end/default.ts
Normal file
23
web/app/components/workflow/nodes/loop-end/default.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import type {
|
||||
SimpleNodeType,
|
||||
} from '@/app/components/workflow/simple-node/types'
|
||||
|
||||
const nodeDefault: NodeDefault<SimpleNodeType> = {
|
||||
defaultValue: {},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes() {
|
||||
return []
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
@@ -138,9 +138,6 @@ const ConditionWrap: FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isSubVariable && (
|
||||
<div className='mx-3 my-2 h-[1px] bg-divider-subtle'></div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@@ -0,0 +1,13 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Empty = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='system-xs-regular flex h-10 items-center justify-center rounded-[10px] bg-background-section text-text-tertiary'>
|
||||
{t('workflow.nodes.loop.setLoopVariables')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Empty
|
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type {
|
||||
LoopVariable,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
import type {
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
ValueType,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
const objectPlaceholder = `# example
|
||||
# {
|
||||
# "name": "ray",
|
||||
# "age": 20
|
||||
# }`
|
||||
const arrayStringPlaceholder = `# example
|
||||
# [
|
||||
# "value1",
|
||||
# "value2"
|
||||
# ]`
|
||||
const arrayNumberPlaceholder = `# example
|
||||
# [
|
||||
# 100,
|
||||
# 200
|
||||
# ]`
|
||||
const arrayObjectPlaceholder = `# example
|
||||
# [
|
||||
# {
|
||||
# "name": "ray",
|
||||
# "age": 20
|
||||
# },
|
||||
# {
|
||||
# "name": "lily",
|
||||
# "age": 18
|
||||
# }
|
||||
# ]`
|
||||
|
||||
type FormItemProps = {
|
||||
nodeId: string
|
||||
item: LoopVariable
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
const FormItem = ({
|
||||
nodeId,
|
||||
item,
|
||||
onChange,
|
||||
}: FormItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { value_type, var_type, value } = item
|
||||
|
||||
const handleInputChange = useCallback((e: any) => {
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
|
||||
const handleChange = useCallback((value: any) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
|
||||
const filterVar = useCallback((variable: Var) => {
|
||||
return variable.type === var_type
|
||||
}, [var_type])
|
||||
|
||||
const editorMinHeight = useMemo(() => {
|
||||
if (var_type === VarType.arrayObject)
|
||||
return '240px'
|
||||
return '120px'
|
||||
}, [var_type])
|
||||
const placeholder = useMemo(() => {
|
||||
if (var_type === VarType.arrayString)
|
||||
return arrayStringPlaceholder
|
||||
if (var_type === VarType.arrayNumber)
|
||||
return arrayNumberPlaceholder
|
||||
if (var_type === VarType.arrayObject)
|
||||
return arrayObjectPlaceholder
|
||||
return objectPlaceholder
|
||||
}, [var_type])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
value_type === ValueType.variable && (
|
||||
<VarReferencePicker
|
||||
readonly={false}
|
||||
nodeId={nodeId}
|
||||
isShowNodeName
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
filterVar={filterVar}
|
||||
placeholder={t('workflow.nodes.assigner.setParameter') as string}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.string && (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
className='min-h-12 w-full'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.number && (
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
className='w-full'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant
|
||||
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
|
||||
&& (
|
||||
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
|
||||
<CodeEditor
|
||||
value={value}
|
||||
isExpand
|
||||
noWrapper
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleChange}
|
||||
className='w-full'
|
||||
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormItem
|
@@ -0,0 +1,28 @@
|
||||
import Empty from './empty'
|
||||
import Item from './item'
|
||||
import type {
|
||||
LoopVariable,
|
||||
LoopVariablesComponentShape,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
|
||||
type LoopVariableProps = {
|
||||
variables?: LoopVariable[]
|
||||
} & LoopVariablesComponentShape
|
||||
|
||||
const LoopVariableComponent = ({
|
||||
variables = [],
|
||||
...restProps
|
||||
}: LoopVariableProps) => {
|
||||
if (!variables.length)
|
||||
return <Empty />
|
||||
|
||||
return variables.map(variable => (
|
||||
<Item
|
||||
key={variable.id}
|
||||
item={variable}
|
||||
{...restProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
export default LoopVariableComponent
|
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
|
||||
type InputModeSelectProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const InputModeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: InputModeSelectProps) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
label: 'Variable',
|
||||
value: 'variable',
|
||||
},
|
||||
{
|
||||
label: 'Constant',
|
||||
value: 'constant',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PureSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
popupProps={{
|
||||
title: t('workflow.nodes.loop.inputMode'),
|
||||
className: 'w-[132px]',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputModeSelect
|
@@ -0,0 +1,78 @@
|
||||
import { useCallback } from 'react'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InputModeSelect from './input-mode-selec'
|
||||
import VariableTypeSelect from './variable-type-select'
|
||||
import FormItem from './form-item'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type {
|
||||
LoopVariable,
|
||||
LoopVariablesComponentShape,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
|
||||
type ItemProps = {
|
||||
item: LoopVariable
|
||||
} & LoopVariablesComponentShape
|
||||
const Item = ({
|
||||
nodeId,
|
||||
item,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleUpdateItemLabel = useCallback((e: any) => {
|
||||
handleUpdateLoopVariable(item.id, { label: e.target.value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemVarType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { var_type: value, value: undefined })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValueType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value_type: value, value: undefined })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValue = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
return (
|
||||
<div className='mb-4 flex last-of-type:mb-0'>
|
||||
<div className='w-0 grow'>
|
||||
<div className='mb-1 grid grid-cols-3 gap-1'>
|
||||
<Input
|
||||
value={item.label}
|
||||
onChange={handleUpdateItemLabel}
|
||||
autoFocus={!item.label}
|
||||
placeholder={t('workflow.nodes.loop.variableName')}
|
||||
/>
|
||||
<VariableTypeSelect
|
||||
value={item.var_type}
|
||||
onChange={handleUpdateItemVarType}
|
||||
/>
|
||||
<InputModeSelect
|
||||
value={item.value_type}
|
||||
onChange={handleUpdateItemValueType}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormItem
|
||||
nodeId={nodeId}
|
||||
item={item}
|
||||
onChange={handleUpdateItemValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
className='shrink-0'
|
||||
size='l'
|
||||
onClick={() => handleRemoveLoopVariable(item.id)}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Item
|
@@ -0,0 +1,51 @@
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type VariableTypeSelectProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const VariableTypeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: VariableTypeSelectProps) => {
|
||||
const options = [
|
||||
{
|
||||
label: 'String',
|
||||
value: VarType.string,
|
||||
},
|
||||
{
|
||||
label: 'Number',
|
||||
value: VarType.number,
|
||||
},
|
||||
{
|
||||
label: 'Object',
|
||||
value: VarType.object,
|
||||
},
|
||||
{
|
||||
label: 'Array[string]',
|
||||
value: VarType.arrayString,
|
||||
},
|
||||
{
|
||||
label: 'Array[number]',
|
||||
value: VarType.arrayNumber,
|
||||
},
|
||||
{
|
||||
label: 'Array[object]',
|
||||
value: VarType.arrayObject,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PureSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
popupProps={{
|
||||
className: 'w-[132px]',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableTypeSelect
|
@@ -28,6 +28,11 @@ const nodeDefault: NodeDefault<LoopNodeType> = {
|
||||
checkValid(payload: LoopNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
payload.loop_variables?.forEach((variable) => {
|
||||
if (!variable.label)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
})
|
||||
|
||||
payload.break_conditions!.forEach((condition) => {
|
||||
if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import Split from '../_base/components/split'
|
||||
import ResultPanel from '../../run/result-panel'
|
||||
import InputNumberWithSlider from '../_base/components/input-number-with-slider'
|
||||
import type { LoopNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import ConditionWrap from './components/condition-wrap'
|
||||
import LoopVariable from './components/loop-variables'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
@@ -45,6 +46,9 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
handleAddLoopVariable,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const nodeInfo = formatTracing(loopRunResult, t)[0]
|
||||
@@ -53,6 +57,27 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t('workflow.nodes.loop.loopVariables')}</div>}
|
||||
operations={
|
||||
<div
|
||||
className='mr-4 flex h-5 w-5 cursor-pointer items-center justify-center'
|
||||
onClick={handleAddLoopVariable}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='px-4'>
|
||||
<LoopVariable
|
||||
variables={inputs.loop_variables}
|
||||
nodeId={id}
|
||||
handleRemoveLoopVariable={handleRemoveLoopVariable}
|
||||
handleUpdateLoopVariable={handleUpdateLoopVariable}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Split className='my-2' />
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>}
|
||||
tooltip={t(`${i18nPrefix}.breakConditionTip`)}
|
||||
@@ -74,7 +99,7 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
logicalOperator={inputs.logical_operator!}
|
||||
/>
|
||||
</Field>
|
||||
<Split />
|
||||
<Split className='mt-2' />
|
||||
<div className='mt-2'>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.loopMaxCount`)}</div>}
|
||||
|
@@ -4,6 +4,7 @@ import type {
|
||||
CommonNodeType,
|
||||
ErrorHandleMode,
|
||||
ValueSelector,
|
||||
ValueType,
|
||||
Var,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
@@ -65,6 +66,13 @@ export type handleRemoveSubVariableCondition = (conditionId: string, subConditio
|
||||
export type HandleUpdateSubVariableCondition = (conditionId: string, subConditionId: string, newSubCondition: Condition) => void
|
||||
export type HandleToggleSubVariableConditionLogicalOperator = (conditionId: string) => void
|
||||
|
||||
export type LoopVariable = {
|
||||
id: string
|
||||
label: string
|
||||
var_type: VarType
|
||||
value_type: ValueType
|
||||
value: any
|
||||
}
|
||||
export type LoopNodeType = CommonNodeType & {
|
||||
startNodeType?: BlockEnum
|
||||
start_node_id: string
|
||||
@@ -73,4 +81,14 @@ export type LoopNodeType = CommonNodeType & {
|
||||
break_conditions?: Condition[]
|
||||
loop_count: number
|
||||
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
|
||||
loop_variables?: LoopVariable[]
|
||||
}
|
||||
|
||||
export type HandleUpdateLoopVariable = (id: string, updateData: Partial<LoopVariable>) => void
|
||||
export type HandleRemoveLoopVariable = (id: string) => void
|
||||
|
||||
export type LoopVariablesComponentShape = {
|
||||
nodeId: string
|
||||
handleRemoveLoopVariable: HandleRemoveLoopVariable
|
||||
handleUpdateLoopVariable: HandleUpdateLoopVariable
|
||||
}
|
||||
|
@@ -1,14 +1,17 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { uuid4 } from '@sentry/utils'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useIsNodeInLoop,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { VarType } from '../../types'
|
||||
import { ValueType, VarType } from '../../types'
|
||||
import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
@@ -27,6 +30,11 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
const conversationVariables = useStore(s => s.conversationVariables)
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
|
||||
const inputsRef = useRef(inputs)
|
||||
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
|
||||
inputsRef.current = newInputs
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
@@ -35,7 +43,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
// output
|
||||
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const loopChildrenNodes = getLoopNodeChildren(id)
|
||||
const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)]
|
||||
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
|
||||
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables)
|
||||
|
||||
@@ -291,6 +299,43 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddLoopVariable = useCallback(() => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (!draft.loop_variables)
|
||||
draft.loop_variables = []
|
||||
|
||||
draft.loop_variables.push({
|
||||
id: uuid4(),
|
||||
label: '',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveLoopVariable = useCallback((id: string) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.loop_variables = draft.loop_variables?.filter(item => item.id !== id)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateLoopVariable = useCallback((id: string, updateData: any) => {
|
||||
const loopVariables = inputsRef.current.loop_variables || []
|
||||
const index = loopVariables.findIndex(item => item.id === id)
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (index > -1) {
|
||||
draft.loop_variables![index] = {
|
||||
...draft.loop_variables![index],
|
||||
...updateData,
|
||||
}
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
@@ -325,6 +370,9 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
changeErrorResponseMode,
|
||||
handleAddLoopVariable,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,10 @@ import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import {
|
||||
LOOP_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
@@ -114,7 +117,7 @@ export const useNodeLoopInteractions = () => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
@@ -7,7 +10,6 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type NodeVariableItemProps = {
|
||||
isEnv: boolean
|
||||
@@ -33,38 +35,75 @@ const NodeVariableItem = ({
|
||||
isException,
|
||||
}: NodeVariableItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const VariableIcon = useMemo(() => {
|
||||
if (isEnv) {
|
||||
return (
|
||||
<Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />
|
||||
)
|
||||
}
|
||||
|
||||
if (isChatVar) {
|
||||
return (
|
||||
<BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Variable02
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 shrink-0 text-text-accent',
|
||||
isException && 'text-text-warning',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}, [isEnv, isChatVar, isException])
|
||||
|
||||
const VariableName = useMemo(() => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium ml-0.5 shrink truncate text-text-accent',
|
||||
isEnv && 'text-gray-900',
|
||||
isException && 'text-text-warning',
|
||||
isChatVar && 'text-util-colors-teal-teal-700',
|
||||
)}
|
||||
title={varName}
|
||||
>
|
||||
{varName}
|
||||
</div>
|
||||
)
|
||||
}, [isEnv, isChatVar, varName, isException])
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative flex items-center gap-1 self-stretch rounded-md bg-workflow-block-parma-bg p-[3px] pl-[5px]',
|
||||
showBorder && '!bg-black/[0.02]',
|
||||
className,
|
||||
)}>
|
||||
{!isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 max-w-[85px] truncate text-xs font-medium text-gray-700' title={node?.data.title}>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex w-full items-center text-primary-600'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-primary-500', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{!isChatVar && <div className={cn('system-xs-medium ml-0.5 max-w-[75px] overflow-hidden truncate text-ellipsis', isEnv && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>}
|
||||
{isChatVar
|
||||
&& <div className='flex w-full items-center gap-1'>
|
||||
<div className='flex h-[18px] min-w-[18px] flex-1 items-center gap-0.5'>
|
||||
<BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />
|
||||
<div className={cn('system-xs-medium ml-0.5 max-w-[75px] overflow-hidden truncate text-ellipsis text-util-colors-teal-teal-700')}>{varName}</div>
|
||||
</div>
|
||||
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
|
||||
</div>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{
|
||||
node && (
|
||||
<>
|
||||
<div className='shrink-0 p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node.data.type}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='mx-0.5 shrink-[1000] truncate text-xs font-medium text-gray-700'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
<Line3 className='mr-0.5 shrink-0'></Line3>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{VariableIcon}
|
||||
{VariableName}
|
||||
</div>
|
||||
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user