feat: Add sub-graph component for workflow

This commit is contained in:
zhsama
2026-01-12 14:56:53 +08:00
parent f925266c1b
commit cab7cd37b8
21 changed files with 1046 additions and 19 deletions

View File

@ -0,0 +1,57 @@
import type { FC } from 'react'
import type { SubGraphConfig } from '../types'
import { memo, useMemo } from 'react'
import { useStore as useReactFlowStore } from 'reactflow'
import { useShallow } from 'zustand/react/shallow'
import { Panel as NodePanel } from '@/app/components/workflow/nodes'
type SubGraphChildrenProps = {
toolNodeId: string
paramKey: string
onConfigChange: (config: Partial<SubGraphConfig>) => void
}
const SubGraphChildren: FC<SubGraphChildrenProps> = ({
toolNodeId: _toolNodeId,
paramKey: _paramKey,
onConfigChange: _onConfigChange,
}) => {
const selectedNode = useReactFlowStore(useShallow((s) => {
const nodes = s.getNodes()
const currentNode = nodes.find(node => node.data.selected)
if (currentNode) {
return {
id: currentNode.id,
type: currentNode.type,
data: currentNode.data,
}
}
return null
}))
const nodePanel = useMemo(() => {
if (!selectedNode)
return null
return (
<NodePanel
id={selectedNode.id}
type={selectedNode.type}
data={selectedNode.data}
/>
)
}, [selectedNode])
return (
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 flex">
{nodePanel && (
<div className="pointer-events-auto">
{nodePanel}
</div>
)}
</div>
)
}
export default memo(SubGraphChildren)

View File

@ -0,0 +1,107 @@
import type { FC } from 'react'
import type { Viewport } from 'reactflow'
import type { SubGraphConfig } from '../types'
import type { Edge, Node } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { useAvailableNodesMetaData, useSubGraphPersistence } from '../hooks'
import SubGraphChildren from './sub-graph-children'
type SubGraphMainProps = {
nodes: Node[]
edges: Edge[]
viewport: Viewport
toolNodeId: string
paramKey: string
}
const SubGraphMain: FC<SubGraphMainProps> = ({
nodes,
edges,
viewport,
toolNodeId,
paramKey,
}) => {
const availableNodesMetaData = useAvailableNodesMetaData()
const {
saveSubGraphData,
loadSubGraphData,
updateSubGraphConfig,
} = useSubGraphPersistence({ toolNodeId, paramKey })
const handleNodesChange = useCallback((updatedNodes: Node[]) => {
const existingData = loadSubGraphData()
const defaultConfig: SubGraphConfig = {
enabled: true,
startNodeId: updatedNodes[0]?.id || '',
selectedOutputVar: [],
whenOutputNone: 'skip',
}
saveSubGraphData({
nodes: updatedNodes,
edges,
config: existingData?.config || defaultConfig,
})
}, [edges, loadSubGraphData, saveSubGraphData])
const hooksStore = useMemo(() => {
return {
availableNodesMetaData,
doSyncWorkflowDraft: async () => {
handleNodesChange(nodes)
},
syncWorkflowDraftWhenPageClose: () => {
handleNodesChange(nodes)
},
handleRefreshWorkflowDraft: () => {},
handleBackupDraft: () => {},
handleLoadBackupDraft: () => {},
handleRestoreFromPublishedWorkflow: () => {},
handleRun: () => {},
handleStopRun: () => {},
handleStartWorkflowRun: () => {},
handleWorkflowStartRunInWorkflow: () => {},
handleWorkflowStartRunInChatflow: () => {},
handleWorkflowTriggerScheduleRunInWorkflow: () => {},
handleWorkflowTriggerWebhookRunInWorkflow: () => {},
handleWorkflowTriggerPluginRunInWorkflow: () => {},
handleWorkflowRunAllTriggersInWorkflow: () => {},
getWorkflowRunAndTraceUrl: () => ({ runUrl: '', traceUrl: '' }),
exportCheck: async () => {},
handleExportDSL: async () => {},
fetchInspectVars: async () => {},
hasNodeInspectVars: () => false,
hasSetInspectVar: () => false,
fetchInspectVarValue: async () => {},
editInspectVarValue: async () => {},
renameInspectVarName: async () => {},
appendNodeInspectVars: () => {},
deleteInspectVar: async () => {},
deleteNodeInspectorVars: async () => {},
deleteAllInspectorVars: async () => {},
isInspectVarEdited: () => false,
resetToLastRunVar: async () => {},
invalidateSysVarValues: () => {},
resetConversationVar: async () => {},
invalidateConversationVarValues: () => {},
}
}, [availableNodesMetaData, handleNodesChange, nodes])
return (
<WorkflowWithInnerContext
nodes={nodes}
edges={edges}
viewport={viewport}
hooksStore={hooksStore as any}
>
<SubGraphChildren
toolNodeId={toolNodeId}
paramKey={paramKey}
onConfigChange={updateSubGraphConfig}
/>
</WorkflowWithInnerContext>
)
}
export default SubGraphMain

View File

@ -0,0 +1,5 @@
export { useAvailableNodesMetaData } from './use-available-nodes-meta-data'
export { useSubGraphInit } from './use-sub-graph-init'
export { useSubGraphNodes } from './use-sub-graph-nodes'
export { useSubGraphPersistence } from './use-sub-graph-persistence'
export type { SubGraphData } from './use-sub-graph-persistence'

View File

@ -0,0 +1,43 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
import { BlockEnum } from '@/app/components/workflow/types'
export const useAvailableNodesMetaData = () => {
const { t } = useTranslation()
const availableNodesMetaData = useMemo(() => WORKFLOW_COMMON_NODES.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
return {
...node,
metaData: {
...metaData,
title,
description,
},
defaultValue: {
...node.defaultValue,
type: metaData.type,
title,
},
}
}), [t])
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
acc![node.metaData.type] = node
return acc
}, {} as AvailableNodesMetaData['nodesMap']), [availableNodesMetaData])
return useMemo(() => {
return {
nodes: availableNodesMetaData,
nodesMap: {
...availableNodesMetaDataMap,
[BlockEnum.VariableAssigner]: availableNodesMetaDataMap?.[BlockEnum.VariableAggregator],
},
}
}, [availableNodesMetaData, availableNodesMetaDataMap])
}

View File

@ -0,0 +1,90 @@
import type { SubGraphProps } from '../types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { Edge, Node } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { BlockEnum, PromptRole } from '@/app/components/workflow/types'
import { AppModeEnum } from '@/types/app'
const SUBGRAPH_SOURCE_NODE_ID = 'subgraph-source'
const SUBGRAPH_LLM_NODE_ID = 'subgraph-llm'
export const useSubGraphInit = (props: SubGraphProps) => {
const { sourceVariable, agentName } = props
const initialNodes = useMemo((): Node[] => {
const sourceVarName = sourceVariable.length > 1
? sourceVariable.slice(1).join('.')
: 'output'
const startNode: Node<StartNodeType> = {
id: SUBGRAPH_SOURCE_NODE_ID,
type: 'custom',
position: { x: 100, y: 150 },
data: {
type: BlockEnum.Start,
title: `${agentName}: ${sourceVarName}`,
desc: 'Source variable from agent',
_connectedSourceHandleIds: ['source'],
_connectedTargetHandleIds: [],
variables: [],
},
}
const llmNode: Node<LLMNodeType> = {
id: SUBGRAPH_LLM_NODE_ID,
type: 'custom',
position: { x: 450, y: 150 },
data: {
type: BlockEnum.LLM,
title: 'LLM',
desc: 'Transform the output',
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: ['target'],
model: {
provider: '',
name: '',
mode: AppModeEnum.CHAT,
completion_params: {
temperature: 0.7,
},
},
prompt_template: [{
role: PromptRole.system,
text: '',
}],
context: {
enabled: false,
variable_selector: [],
},
vision: {
enabled: false,
},
},
}
return [startNode, llmNode]
}, [sourceVariable, agentName])
const initialEdges = useMemo((): Edge[] => {
return [
{
id: `${SUBGRAPH_SOURCE_NODE_ID}-${SUBGRAPH_LLM_NODE_ID}`,
source: SUBGRAPH_SOURCE_NODE_ID,
sourceHandle: 'source',
target: SUBGRAPH_LLM_NODE_ID,
targetHandle: 'target',
type: 'custom',
data: {
sourceType: BlockEnum.Start,
targetType: BlockEnum.LLM,
},
},
]
}, [])
return {
initialNodes,
initialEdges,
}
}

View File

@ -0,0 +1,20 @@
import type { Edge, Node } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { initialEdges, initialNodes } from '@/app/components/workflow/utils'
export const useSubGraphNodes = (nodes: Node[], edges: Edge[]) => {
const processedNodes = useMemo(
() => initialNodes(nodes, edges),
[nodes, edges],
)
const processedEdges = useMemo(
() => initialEdges(edges, nodes),
[edges, nodes],
)
return {
nodes: processedNodes,
edges: processedEdges,
}
}

View File

@ -0,0 +1,128 @@
import type { SubGraphConfig } from '../types'
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
import type { Edge, Node } from '@/app/components/workflow/types'
import { useCallback } from 'react'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
type SubGraphPersistenceProps = {
toolNodeId: string
paramKey: string
}
export type SubGraphData = {
nodes: Node[]
edges: Edge[]
config: SubGraphConfig
}
const SUB_GRAPH_DATA_PREFIX = '__subgraph__'
export const useSubGraphPersistence = ({
toolNodeId,
paramKey,
}: SubGraphPersistenceProps) => {
const { inputs, setInputs } = useNodeCrud<ToolNodeType>(toolNodeId, {} as ToolNodeType)
const getSubGraphDataKey = useCallback(() => {
return `${SUB_GRAPH_DATA_PREFIX}${paramKey}`
}, [paramKey])
const loadSubGraphData = useCallback((): SubGraphData | null => {
const dataKey = getSubGraphDataKey()
const toolParameters = inputs.tool_parameters || {}
const storedData = toolParameters[dataKey]
if (!storedData || storedData.type !== VarKindType.constant) {
return null
}
try {
const parsed = typeof storedData.value === 'string'
? JSON.parse(storedData.value)
: storedData.value
return parsed as SubGraphData
}
catch {
return null
}
}, [getSubGraphDataKey, inputs.tool_parameters])
const saveSubGraphData = useCallback((data: SubGraphData) => {
const dataKey = getSubGraphDataKey()
const newToolParameters = {
...inputs.tool_parameters,
[dataKey]: {
type: VarKindType.constant,
value: JSON.stringify(data),
},
}
setInputs({
...inputs,
tool_parameters: newToolParameters,
})
}, [getSubGraphDataKey, inputs, setInputs])
const clearSubGraphData = useCallback(() => {
const dataKey = getSubGraphDataKey()
const newToolParameters = { ...inputs.tool_parameters }
delete newToolParameters[dataKey]
setInputs({
...inputs,
tool_parameters: newToolParameters,
})
}, [getSubGraphDataKey, inputs, setInputs])
const hasSubGraphData = useCallback(() => {
const dataKey = getSubGraphDataKey()
const toolParameters = inputs.tool_parameters || {}
return !!toolParameters[dataKey]
}, [getSubGraphDataKey, inputs.tool_parameters])
const updateSubGraphConfig = useCallback((
config: Partial<SubGraphConfig>,
) => {
const existingData = loadSubGraphData()
if (!existingData)
return
saveSubGraphData({
...existingData,
config: {
...existingData.config,
...config,
},
})
}, [loadSubGraphData, saveSubGraphData])
const updateSubGraphNodes = useCallback((
nodes: Node[],
edges: Edge[],
) => {
const existingData = loadSubGraphData()
const defaultConfig: SubGraphConfig = {
enabled: true,
startNodeId: nodes[0]?.id || '',
selectedOutputVar: [],
whenOutputNone: 'skip',
}
saveSubGraphData({
nodes,
edges,
config: existingData?.config || defaultConfig,
})
}, [loadSubGraphData, saveSubGraphData])
return {
loadSubGraphData,
saveSubGraphData,
clearSubGraphData,
hasSubGraphData,
updateSubGraphConfig,
updateSubGraphNodes,
}
}

View File

@ -0,0 +1,57 @@
import type { FC } from 'react'
import type { Viewport } from 'reactflow'
import type { SubGraphProps } from './types'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
import { memo, useMemo } from 'react'
import WorkflowWithDefaultContext from '@/app/components/workflow'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import SubGraphMain from './components/sub-graph-main'
import { useSubGraphInit, useSubGraphNodes, useSubGraphPersistence } from './hooks'
import { createSubGraphSlice } from './store'
const defaultViewport: Viewport = {
x: 50,
y: 50,
zoom: 1,
}
const SubGraph: FC<SubGraphProps> = (props) => {
const { toolNodeId, paramKey } = props
const { loadSubGraphData } = useSubGraphPersistence({ toolNodeId, paramKey })
const savedData = useMemo(() => loadSubGraphData(), [loadSubGraphData])
const { initialNodes, initialEdges } = useSubGraphInit(props)
const nodesSource = savedData?.nodes || initialNodes
const edgesSource = savedData?.edges || initialEdges
const { nodes, edges } = useSubGraphNodes(nodesSource, edgesSource)
return (
<WorkflowWithDefaultContext
nodes={nodes}
edges={edges}
>
<SubGraphMain
nodes={nodes}
edges={edges}
viewport={defaultViewport}
toolNodeId={toolNodeId}
paramKey={paramKey}
/>
</WorkflowWithDefaultContext>
)
}
const SubGraphWrapper: FC<SubGraphProps> = (props) => {
return (
<WorkflowContextProvider
injectWorkflowStoreSliceFn={createSubGraphSlice as unknown as InjectWorkflowStoreSliceFn}
>
<SubGraph {...props} />
</WorkflowContextProvider>
)
}
export default memo(SubGraphWrapper)

View File

@ -0,0 +1,49 @@
import type { CreateSubGraphSlice, SubGraphSliceShape } from '../types'
const initialState: Omit<SubGraphSliceShape, 'setSubGraphContext' | 'setSubGraphNodes' | 'setSubGraphEdges' | 'setSelectedOutputVar' | 'setWhenOutputNone' | 'setDefaultValue' | 'setShowDebugPanel' | 'setIsRunning' | 'setParentAvailableVars' | 'resetSubGraph'> = {
parentToolNodeId: '',
parameterKey: '',
sourceAgentNodeId: '',
sourceVariable: [],
subGraphNodes: [],
subGraphEdges: [],
selectedOutputVar: [],
whenOutputNone: 'skip',
defaultValue: '',
showDebugPanel: false,
isRunning: false,
parentAvailableVars: [],
}
export const createSubGraphSlice: CreateSubGraphSlice = set => ({
...initialState,
setSubGraphContext: context => set(() => ({
parentToolNodeId: context.parentToolNodeId,
parameterKey: context.parameterKey,
sourceAgentNodeId: context.sourceAgentNodeId,
sourceVariable: context.sourceVariable,
})),
setSubGraphNodes: nodes => set(() => ({ subGraphNodes: nodes })),
setSubGraphEdges: edges => set(() => ({ subGraphEdges: edges })),
setSelectedOutputVar: selector => set(() => ({ selectedOutputVar: selector })),
setWhenOutputNone: option => set(() => ({ whenOutputNone: option })),
setDefaultValue: value => set(() => ({ defaultValue: value })),
setShowDebugPanel: show => set(() => ({ showDebugPanel: show })),
setIsRunning: running => set(() => ({ isRunning: running })),
setParentAvailableVars: vars => set(() => ({ parentAvailableVars: vars })),
resetSubGraph: () => set(() => ({ ...initialState })),
})

View File

@ -0,0 +1,65 @@
import type { StateCreator } from 'zustand'
import type { Edge, Node, NodeOutPutVar, ValueSelector, VarType } from '@/app/components/workflow/types'
export type WhenOutputNoneOption = 'skip' | 'error' | 'default'
export type SubGraphConfig = {
enabled: boolean
startNodeId: string
selectedOutputVar: ValueSelector
whenOutputNone: WhenOutputNoneOption
defaultValue?: string
}
export type SubGraphOutputVariable = {
nodeId: string
nodeName: string
variable: string
type: VarType
description?: string
}
export type SubGraphProps = {
toolNodeId: string
paramKey: string
sourceVariable: ValueSelector
agentNodeId: string
agentName: string
}
export type SubGraphSliceShape = {
parentToolNodeId: string
parameterKey: string
sourceAgentNodeId: string
sourceVariable: ValueSelector
subGraphNodes: Node[]
subGraphEdges: Edge[]
selectedOutputVar: ValueSelector
whenOutputNone: WhenOutputNoneOption
defaultValue: string
showDebugPanel: boolean
isRunning: boolean
parentAvailableVars: NodeOutPutVar[]
setSubGraphContext: (context: {
parentToolNodeId: string
parameterKey: string
sourceAgentNodeId: string
sourceVariable: ValueSelector
}) => void
setSubGraphNodes: (nodes: Node[]) => void
setSubGraphEdges: (edges: Edge[]) => void
setSelectedOutputVar: (selector: ValueSelector) => void
setWhenOutputNone: (option: WhenOutputNoneOption) => void
setDefaultValue: (value: string) => void
setShowDebugPanel: (show: boolean) => void
setIsRunning: (running: boolean) => void
setParentAvailableVars: (vars: NodeOutPutVar[]) => void
resetSubGraph: () => void
}
export type CreateSubGraphSlice = StateCreator<SubGraphSliceShape>