mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 19:36:53 +08:00
Feat/loop break node (#17268)
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
ListFilter,
|
||||
Llm,
|
||||
Loop,
|
||||
LoopEnd,
|
||||
ParameterExtractor,
|
||||
QuestionClassifier,
|
||||
TemplatingTransform,
|
||||
@@ -54,6 +55,7 @@ const getIcon = (type: BlockEnum, className: string) => {
|
||||
[BlockEnum.Iteration]: <Iteration className={className} />,
|
||||
[BlockEnum.LoopStart]: <VariableX className={className} />,
|
||||
[BlockEnum.Loop]: <Loop className={className} />,
|
||||
[BlockEnum.LoopEnd]: <LoopEnd className={className} />,
|
||||
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
|
||||
[BlockEnum.DocExtractor]: <DocsExtractor className={className} />,
|
||||
[BlockEnum.ListFilter]: <ListFilter className={className} />,
|
||||
@@ -68,6 +70,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.Loop]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.LoopEnd]: 'bg-util-colors-warning-warning-500',
|
||||
[BlockEnum.HttpRequest]: 'bg-util-colors-violet-violet-500',
|
||||
[BlockEnum.Answer]: 'bg-util-colors-warning-warning-500',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'bg-util-colors-green-green-500',
|
||||
|
||||
@@ -15,6 +15,7 @@ import { BLOCK_CLASSIFICATIONS } from './constants'
|
||||
import { useBlocks } from './hooks'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type BlocksProps = {
|
||||
searchText: string
|
||||
@@ -90,7 +91,15 @@ const Blocks = ({
|
||||
className='mr-2 shrink-0'
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='text-sm text-text-secondary'>{block.title}</div>
|
||||
<div className='grow text-sm text-text-secondary'>{block.title}</div>
|
||||
{
|
||||
block.type === BlockEnum.LoopEnd && (
|
||||
<Badge
|
||||
text={t('workflow.nodes.loop.loopNode')}
|
||||
className='ml-2 shrink-0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
|
||||
@@ -39,6 +39,12 @@ export const BLOCKS: Block[] = [
|
||||
type: BlockEnum.IfElse,
|
||||
title: 'IF/ELSE',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: 'Exit Loop',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.Iteration,
|
||||
|
||||
@@ -21,6 +21,7 @@ import ListFilterDefault from './nodes/list-operator/default'
|
||||
import IterationStartDefault from './nodes/iteration-start/default'
|
||||
import AgentDefault from './nodes/agent/default'
|
||||
import LoopStartDefault from './nodes/loop-start/default'
|
||||
import LoopEndDefault from './nodes/loop-end/default'
|
||||
|
||||
type NodesExtraData = {
|
||||
author: string
|
||||
@@ -122,6 +123,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
|
||||
getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes,
|
||||
checkValid: LoopStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopEndDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopEndDefault.getAvailableNextNodes,
|
||||
checkValid: LoopEndDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
@@ -297,6 +307,12 @@ export const NODES_INITIAL_DATA = {
|
||||
desc: '',
|
||||
...LoopStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopEndDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
type: BlockEnum.Code,
|
||||
title: '',
|
||||
|
||||
@@ -42,6 +42,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType)
|
||||
return []
|
||||
|
||||
return nodesExtraData[nodeType].availableNextNodes || []
|
||||
}, [nodeType, nodesExtraData])
|
||||
|
||||
@@ -54,6 +55,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}),
|
||||
availableNextBlocks: availableNextBlocks.filter((nType) => {
|
||||
@@ -63,6 +67,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import {
|
||||
genNewNodeTitleFromOld,
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getTopLeftNodePosition,
|
||||
} from '../utils'
|
||||
@@ -638,7 +639,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
if (node.id === currentNode.parentId)
|
||||
node.data._children = node.data._children?.filter(child => child !== nodeId)
|
||||
node.data._children = node.data._children?.filter(child => child.nodeId !== nodeId)
|
||||
})
|
||||
draft.splice(currentNodeIndex, 1)
|
||||
})
|
||||
@@ -686,6 +687,7 @@ export const useNodesInteractions = () => {
|
||||
newIterationStartNode,
|
||||
newLoopStartNode,
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
@@ -775,10 +777,10 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
|
||||
if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
})
|
||||
draft.push(newNode)
|
||||
|
||||
@@ -853,7 +855,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
let newEdge
|
||||
|
||||
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
|
||||
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier) && (nodeType !== BlockEnum.LoopEnd)) {
|
||||
newEdge = {
|
||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||
type: CUSTOM_EDGE,
|
||||
@@ -901,7 +903,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
|
||||
if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) {
|
||||
node.data.start_node_id = newNode.id
|
||||
@@ -909,7 +911,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.Loop && nextNode.parentId === node.id)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
|
||||
if (node.data.type === BlockEnum.Loop && node.data.start_node_id === nextNodeId) {
|
||||
node.data.start_node_id = newNode.id
|
||||
@@ -1004,7 +1006,7 @@ export const useNodesInteractions = () => {
|
||||
const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration
|
||||
const isNextNodeInLoop = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop
|
||||
|
||||
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
|
||||
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.LoopEnd) {
|
||||
newNextEdge = {
|
||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||
type: CUSTOM_EDGE,
|
||||
@@ -1049,9 +1051,9 @@ export const useNodesInteractions = () => {
|
||||
node.position.x += NODE_WIDTH_X_OFFSET
|
||||
|
||||
if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode)
|
||||
@@ -1117,6 +1119,7 @@ export const useNodesInteractions = () => {
|
||||
newIterationStartNode,
|
||||
newLoopStartNode,
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
@@ -1240,7 +1243,7 @@ export const useNodesInteractions = () => {
|
||||
if (nodeId) {
|
||||
// If nodeId is provided, copy that specific node
|
||||
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start
|
||||
&& node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE)
|
||||
&& node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE && node.data.type !== BlockEnum.LoopEnd)
|
||||
if (nodeToCopy)
|
||||
setClipboardElements([nodeToCopy])
|
||||
}
|
||||
@@ -1254,7 +1257,7 @@ export const useNodesInteractions = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.LoopEnd)
|
||||
|
||||
if (selectedNode)
|
||||
setClipboardElements([selectedNode])
|
||||
@@ -1328,7 +1331,7 @@ export const useNodesInteractions = () => {
|
||||
newChildren = copyChildren
|
||||
idMapping = newIdMapping
|
||||
newChildren.forEach((child) => {
|
||||
newNode.data._children?.push(child.id)
|
||||
newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
|
||||
})
|
||||
newChildren.push(newIterationStartNode!)
|
||||
}
|
||||
@@ -1339,7 +1342,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id)
|
||||
newChildren.forEach((child) => {
|
||||
newNode.data._children?.push(child.id)
|
||||
newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
|
||||
})
|
||||
newChildren.push(newLoopStartNode!)
|
||||
}
|
||||
@@ -1424,7 +1427,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const childrenNodes = nodes.filter(n => currentNode.data._children?.includes(n.id))
|
||||
const childrenNodes = nodes.filter(n => currentNode.data._children?.find((c: any) => c.nodeId === n.id))
|
||||
let rightNode: Node
|
||||
let bottomNode: Node
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ export const useWorkflowNodeIterationFinished = () => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
@@ -38,6 +40,18 @@ export const useWorkflowNodeIterationFinished = () => {
|
||||
currentNode.data._runningStatus = data.status
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter((edge) => {
|
||||
return edge.target === data.node_id
|
||||
})
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_targetRunningStatus: data.status as any,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useStoreApi } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import type { LoopFinishedResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useWorkflowNodeLoopFinished = () => {
|
||||
const store = useStoreApi()
|
||||
@@ -14,11 +13,12 @@ export const useWorkflowNodeLoopFinished = () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
@@ -31,13 +31,24 @@ export const useWorkflowNodeLoopFinished = () => {
|
||||
}
|
||||
}
|
||||
}))
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
|
||||
currentNode.data._runningStatus = data.status
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter((edge) => {
|
||||
return edge.target === data.node_id
|
||||
})
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_targetRunningStatus: data.status as any,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,18 +2,12 @@ import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import type { LoopNextResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
export const useWorkflowNodeLoopNext = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeLoopNext = useCallback((params: LoopNextResponse) => {
|
||||
const {
|
||||
loopTimes,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const { data } = params
|
||||
const {
|
||||
getNodes,
|
||||
@@ -23,11 +17,17 @@ export const useWorkflowNodeLoopNext = () => {
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
currentNode.data._loopIndex = loopTimes
|
||||
setLoopTimes(loopTimes + 1)
|
||||
currentNode.data._loopIndex = data.index
|
||||
|
||||
draft.forEach((node) => {
|
||||
if (node.parentId === data.node_id) {
|
||||
node.data._waitingRun = true
|
||||
node.data._runningStatus = NodeRunningStatus.Waiting
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [workflowStore, store])
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeLoopNext,
|
||||
|
||||
@@ -7,7 +7,6 @@ import produce from 'immer'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { LoopStartedResponse } from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useWorkflowNodeLoopStarted = () => {
|
||||
const store = useStoreApi()
|
||||
@@ -25,7 +24,6 @@ export const useWorkflowNodeLoopStarted = () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
@@ -41,7 +39,6 @@ export const useWorkflowNodeLoopStarted = () => {
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
}))
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
|
||||
@@ -61,6 +61,8 @@ import CustomIterationStartNode from './nodes/iteration-start'
|
||||
import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
|
||||
import CustomLoopStartNode from './nodes/loop-start'
|
||||
import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
|
||||
import CustomSimpleNode from './simple-node'
|
||||
import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
|
||||
import Operator from './operator'
|
||||
import CustomEdge from './custom-edge'
|
||||
import CustomConnectionLine from './custom-connection-line'
|
||||
@@ -104,6 +106,7 @@ import DatasetsDetailProvider from './datasets-detail-store/provider'
|
||||
const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
||||
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
||||
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../utils'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
@@ -56,6 +57,7 @@ const AddBlock = ({
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(type),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
@@ -40,10 +41,12 @@ export const useLogs = () => {
|
||||
}] = useBoolean(false)
|
||||
const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([])
|
||||
const [loopResultDurationMap, setLoopResultDurationMap] = useState<LoopDurationMap>({})
|
||||
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => {
|
||||
const [loopResultVariableMap, setLoopResultVariableMap] = useState<Record<string, any>>({})
|
||||
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => {
|
||||
setShowLoopingDetailTrue()
|
||||
setLoopResultList(detail)
|
||||
setLoopResultDurationMap(loopDurationMap)
|
||||
setLoopResultVariableMap(loopVariableMap)
|
||||
}, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap])
|
||||
|
||||
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
|
||||
@@ -101,6 +104,8 @@ export const useLogs = () => {
|
||||
setLoopResultList,
|
||||
loopResultDurationMap,
|
||||
setLoopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
setLoopResultVariableMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
|
||||
@@ -3,13 +3,14 @@ import { RiArrowRightSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type {
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type LoopLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap) => void
|
||||
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
}
|
||||
const LoopLogTrigger = ({
|
||||
nodeInfo,
|
||||
@@ -35,7 +36,11 @@ const LoopLogTrigger = ({
|
||||
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowLoopResultList(nodeInfo.details || [], nodeInfo?.loopDurationMap || nodeInfo.execution_metadata?.loop_duration_map || {})
|
||||
onShowLoopResultList(
|
||||
nodeInfo.details || [],
|
||||
nodeInfo?.loopDurationMap || nodeInfo.execution_metadata?.loop_duration_map || {},
|
||||
nodeInfo.execution_metadata?.loop_variable_map || {},
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -12,19 +12,23 @@ import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { LoopDurationMap, NodeTracing } from '@/types/workflow'
|
||||
import type { LoopDurationMap, LoopVariableMap, NodeTracing } from '@/types/workflow'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onBack: () => void
|
||||
loopDurationMap?: LoopDurationMap
|
||||
loopVariableMap?: LoopVariableMap
|
||||
}
|
||||
|
||||
const LoopResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
loopDurationMap,
|
||||
loopVariableMap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({})
|
||||
@@ -114,6 +118,20 @@ const LoopResultPanel: FC<Props> = ({
|
||||
'overflow-hidden transition-all duration-200',
|
||||
expandedLoops[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
)}>
|
||||
{
|
||||
loopVariableMap?.[index] && (
|
||||
<div className='p-2 pb-0'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.nodes.loop.loopVariables').toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
height={112}
|
||||
value={loopVariableMap[index]}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<TracingPanel
|
||||
list={loop}
|
||||
className='bg-background-section-burn'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiArrowRightSLine,
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
@@ -35,7 +36,7 @@ type Props = {
|
||||
hideInfo?: boolean
|
||||
hideProcessDetail?: boolean
|
||||
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => void
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
notShowIterationNav?: boolean
|
||||
@@ -90,6 +91,20 @@ const NodePanel: FC<Props> = ({
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
|
||||
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
|
||||
|
||||
const inputsTitle = useMemo(() => {
|
||||
let text = t('workflow.common.input')
|
||||
if (nodeInfo.node_type === BlockEnum.Loop)
|
||||
text = t('workflow.nodes.loop.initialLoopVariables')
|
||||
return text.toLocaleUpperCase()
|
||||
}, [nodeInfo.node_type, t])
|
||||
const processDataTitle = t('workflow.common.processData').toLocaleUpperCase()
|
||||
const outputTitle = useMemo(() => {
|
||||
let text = t('workflow.common.output')
|
||||
if (nodeInfo.node_type === BlockEnum.Loop)
|
||||
text = t('workflow.nodes.loop.finalLoopVariables')
|
||||
return text.toLocaleUpperCase()
|
||||
}, [nodeInfo.node_type, t])
|
||||
|
||||
return (
|
||||
<div className={cn('px-2 py-1', className)}>
|
||||
<div className='group rounded-[10px] border border-components-panel-border bg-background-default shadow-xs transition-all hover:shadow-md'>
|
||||
@@ -199,7 +214,7 @@ const NodePanel: FC<Props> = ({
|
||||
<div className={cn('mb-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.input').toLocaleUpperCase()}</div>}
|
||||
title={<div>{inputsTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.inputs}
|
||||
isJSONStringifyBeauty
|
||||
@@ -210,7 +225,7 @@ const NodePanel: FC<Props> = ({
|
||||
<div className={cn('mb-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.processData').toLocaleUpperCase()}</div>}
|
||||
title={<div>{processDataTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.process_data}
|
||||
isJSONStringifyBeauty
|
||||
@@ -221,7 +236,7 @@ const NodePanel: FC<Props> = ({
|
||||
<div>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.output').toLocaleUpperCase()}</div>}
|
||||
title={<div>{outputTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.outputs}
|
||||
isJSONStringifyBeauty
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
@@ -23,6 +24,7 @@ export type SpecialResultPanelProps = {
|
||||
setShowLoopingDetailFalse?: () => void
|
||||
loopResultList?: NodeTracing[][]
|
||||
loopResultDurationMap?: LoopDurationMap
|
||||
loopResultVariableMap?: LoopVariableMap
|
||||
|
||||
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
|
||||
@@ -42,6 +44,7 @@ const SpecialResultPanel = ({
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
@@ -75,6 +78,7 @@ const SpecialResultPanel = ({
|
||||
list={loopResultList}
|
||||
onBack={setShowLoopingDetailFalse}
|
||||
loopDurationMap={loopResultDurationMap}
|
||||
loopVariableMap={loopResultVariableMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
@@ -172,6 +173,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
setShowLoopingDetailFalse={setShowLoopingDetailFalse}
|
||||
loopResultList={loopResultList}
|
||||
loopResultDurationMap={loopResultDurationMap}
|
||||
loopResultVariableMap={loopResultVariableMap}
|
||||
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
|
||||
1
web/app/components/workflow/simple-node/constants.ts
Normal file
1
web/app/components/workflow/simple-node/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const CUSTOM_SIMPLE_NODE = 'custom-simple'
|
||||
148
web/app/components/workflow/simple-node/index.tsx
Normal file
148
web/app/components/workflow/simple-node/index.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import type {
|
||||
FC,
|
||||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
NodeTargetHandle,
|
||||
} from '@/app/components/workflow/nodes/_base/components/node-handle'
|
||||
import NodeControl from '@/app/components/workflow/nodes/_base/components/node-control'
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import type {
|
||||
NodeProps,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
NodeRunningStatus,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
type SimpleNodeProps = NodeProps
|
||||
|
||||
const SimpleNode: FC<SimpleNodeProps> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
|
||||
const {
|
||||
showRunningBorder,
|
||||
showSuccessBorder,
|
||||
showFailedBorder,
|
||||
showExceptionBorder,
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
|
||||
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
|
||||
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
|
||||
showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex rounded-2xl border-[2px]',
|
||||
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
|
||||
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
|
||||
data._waitingRun && 'opacity-70',
|
||||
)}
|
||||
style={{
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative pb-1 shadow-xs',
|
||||
'rounded-[15px] border border-transparent',
|
||||
'w-[240px] bg-workflow-block-bg',
|
||||
!data._runningStatus && 'hover:shadow-lg',
|
||||
showRunningBorder && '!border-state-accent-solid',
|
||||
showSuccessBorder && '!border-state-success-solid',
|
||||
showFailedBorder && '!border-state-destructive-solid',
|
||||
showExceptionBorder && '!border-state-warning-solid',
|
||||
data._isBundled && '!shadow-lg',
|
||||
)}
|
||||
>
|
||||
{
|
||||
data._inParallelHovering && (
|
||||
<div className='top system-2xs-medium-uppercase absolute -top-2.5 left-2 z-10 text-text-tertiary'>
|
||||
{t('workflow.common.parallelRun')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!data._isCandidate && (
|
||||
<NodeTargetHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleClassName='!top-4 !-left-[9px] !translate-y-0'
|
||||
handleId='target'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!data._runningStatus && !nodesReadOnly && !data._isCandidate && (
|
||||
<NodeControl
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className={cn(
|
||||
'flex items-center rounded-t-2xl px-3 pb-2 pt-3',
|
||||
)}>
|
||||
<BlockIcon
|
||||
className='mr-2 shrink-0'
|
||||
type={data.type}
|
||||
size='md'
|
||||
/>
|
||||
<div
|
||||
title={data.title}
|
||||
className='system-sm-semibold-uppercase mr-1 flex grow items-center truncate text-text-primary'
|
||||
>
|
||||
<div>
|
||||
{data.title}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
<RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Succeeded && (
|
||||
<RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Failed && (
|
||||
<RiErrorWarningFill className='h-3.5 w-3.5 text-text-destructive' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Exception && (
|
||||
<RiAlertFill className='h-3.5 w-3.5 text-text-warning-secondary' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SimpleNode)
|
||||
3
web/app/components/workflow/simple-node/types.ts
Normal file
3
web/app/components/workflow/simple-node/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type SimpleNodeType = CommonNodeType
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
ErrorHandleTypeEnum,
|
||||
} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
|
||||
import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
export enum BlockEnum {
|
||||
Start = 'start',
|
||||
@@ -38,6 +39,7 @@ export enum BlockEnum {
|
||||
Agent = 'agent',
|
||||
Loop = 'loop',
|
||||
LoopStart = 'loop-start',
|
||||
LoopEnd = 'loop-end',
|
||||
}
|
||||
|
||||
export enum ControlMode {
|
||||
@@ -64,7 +66,7 @@ export type CommonNodeType<T = {}> = {
|
||||
_singleRunningStatus?: NodeRunningStatus
|
||||
_isCandidate?: boolean
|
||||
_isBundled?: boolean
|
||||
_children?: string[]
|
||||
_children?: { nodeId: string; nodeType: BlockEnum }[]
|
||||
_isEntering?: boolean
|
||||
_showAddVariablePopup?: boolean
|
||||
_holdAddVariablePopup?: boolean
|
||||
@@ -256,16 +258,23 @@ export enum VarType {
|
||||
any = 'any',
|
||||
}
|
||||
|
||||
export enum ValueType {
|
||||
variable = 'variable',
|
||||
constant = 'constant',
|
||||
}
|
||||
|
||||
export type Var = {
|
||||
variable: string
|
||||
type: VarType
|
||||
children?: Var[] // if type is obj, has the children struct
|
||||
children?: Var[] | StructuredOutput // if type is obj, has the children struct
|
||||
isParagraph?: boolean
|
||||
isSelect?: boolean
|
||||
options?: string[]
|
||||
required?: boolean
|
||||
des?: string
|
||||
isException?: boolean
|
||||
isLoopVariable?: boolean
|
||||
nodeId?: string
|
||||
}
|
||||
|
||||
export type NodeOutPutVar = {
|
||||
@@ -273,6 +282,7 @@ export type NodeOutPutVar = {
|
||||
title: string
|
||||
vars: Var[]
|
||||
isStartNode?: boolean
|
||||
isLoop?: boolean
|
||||
}
|
||||
|
||||
export type Block = {
|
||||
|
||||
@@ -49,6 +49,7 @@ import type { LoopNodeType } from './nodes/loop/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { canFindTool, correctModelProvider } from '@/utils'
|
||||
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
|
||||
|
||||
const WHITE = 'WHITE'
|
||||
const GRAY = 'GRAY'
|
||||
@@ -165,7 +166,7 @@ export function generateNewNode({ data, position, id, zIndex, type, ...rest }: O
|
||||
if (data.type === BlockEnum.Iteration) {
|
||||
const newIterationStartNode = getIterationStartNode(newNode.id);
|
||||
(newNode.data as IterationNodeType).start_node_id = newIterationStartNode.id;
|
||||
(newNode.data as IterationNodeType)._children = [newIterationStartNode.id]
|
||||
(newNode.data as IterationNodeType)._children = [{ nodeId: newIterationStartNode.id, nodeType: BlockEnum.IterationStart }]
|
||||
return {
|
||||
newNode,
|
||||
newIterationStartNode,
|
||||
@@ -175,7 +176,7 @@ export function generateNewNode({ data, position, id, zIndex, type, ...rest }: O
|
||||
if (data.type === BlockEnum.Loop) {
|
||||
const newLoopStartNode = getLoopStartNode(newNode.id);
|
||||
(newNode.data as LoopNodeType).start_node_id = newLoopStartNode.id;
|
||||
(newNode.data as LoopNodeType)._children = [newLoopStartNode.id]
|
||||
(newNode.data as LoopNodeType)._children = [{ nodeId: newLoopStartNode.id, nodeType: BlockEnum.LoopStart }]
|
||||
return {
|
||||
newNode,
|
||||
newLoopStartNode,
|
||||
@@ -317,12 +318,12 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
const iterationOrLoopNodeMap = nodes.reduce((acc, node) => {
|
||||
if (node.parentId) {
|
||||
if (acc[node.parentId])
|
||||
acc[node.parentId].push(node.id)
|
||||
acc[node.parentId].push({ nodeId: node.id, nodeType: node.data.type })
|
||||
else
|
||||
acc[node.parentId] = [node.id]
|
||||
acc[node.parentId] = [{ nodeId: node.id, nodeType: node.data.type }]
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, string[]>)
|
||||
}, {} as Record<string, { nodeId: string; nodeType: BlockEnum }[]>)
|
||||
|
||||
return nodes.map((node) => {
|
||||
if (!node.type)
|
||||
@@ -1052,3 +1053,8 @@ export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
|
||||
export const hasRetryNode = (nodeType?: BlockEnum) => {
|
||||
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
|
||||
}
|
||||
|
||||
export const getNodeCustomTypeByNodeDataType = (nodeType: BlockEnum) => {
|
||||
if (nodeType === BlockEnum.LoopEnd)
|
||||
return CUSTOM_SIMPLE_NODE
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user