feat: add AssembleVariablesAlt icon and integrate into sub-graph

components.
This commit is contained in:
zhsama
2026-01-19 22:29:28 +08:00
parent 1bdc47220b
commit f44305af0d
21 changed files with 611 additions and 252 deletions

View File

@ -7,22 +7,28 @@ import { useShallow } from 'zustand/react/shallow'
import { useIsChatMode, useWorkflowVariables } from '@/app/components/workflow/hooks'
import Panel from '@/app/components/workflow/panel'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import ConfigPanel from './config-panel'
type SubGraphChildrenProps = {
agentName: string
extractorNodeId: string
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
}
type SubGraphChildrenProps
= | {
variant: 'agent'
title: string
extractorNodeId: string
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
}
| {
variant: 'assemble'
title: string
extractorNodeId: string
}
const SubGraphChildren: FC<SubGraphChildrenProps> = ({
agentName,
extractorNodeId,
mentionConfig,
onMentionConfigChange,
}) => {
const SubGraphChildren: FC<SubGraphChildrenProps> = (props) => {
const {
variant,
title,
extractorNodeId,
} = props
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const nodePanelWidth = useStore(s => s.nodePanelWidth)
@ -32,7 +38,7 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
}))
const extractorNode = useReactFlowStore(useShallow((s) => {
return s.getNodes().find(node => node.data.type === BlockEnum.LLM)
return s.getNodes().find(node => node.id === extractorNodeId)
}))
const availableNodes = useMemo(() => {
@ -51,8 +57,10 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
return vars.filter(item => item.nodeId === extractorNode.id)
}, [extractorNode, getNodeAvailableVars, isChatMode])
const agentProps = variant === 'agent' ? props : null
const panelRight = useMemo(() => {
if (selectedNode)
if (!agentProps || selectedNode)
return null
return (
@ -62,17 +70,25 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
style={{ width: `${nodePanelWidth}px` }}
>
<ConfigPanel
agentName={agentName}
agentName={title}
extractorNodeId={extractorNodeId}
mentionConfig={mentionConfig}
mentionConfig={agentProps.mentionConfig}
availableNodes={availableNodes}
availableVars={availableVars}
onMentionConfigChange={onMentionConfigChange}
onMentionConfigChange={agentProps.onMentionConfigChange}
/>
</div>
</div>
)
}, [agentName, availableNodes, availableVars, extractorNodeId, mentionConfig, nodePanelWidth, onMentionConfigChange, selectedNode])
}, [agentProps, availableNodes, availableVars, extractorNodeId, nodePanelWidth, selectedNode, title])
if (variant === 'assemble') {
return (
<Panel
withHeader={false}
/>
)
}
return (
<Panel

View File

@ -9,35 +9,46 @@ import { useStoreApi } from 'reactflow'
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
import { useInspectVarsCrudCommon } from '@/app/components/workflow/hooks/use-inspect-vars-crud-common'
import { BlockEnum } from '@/app/components/workflow/types'
import { FlowType } from '@/types/common'
import { useAvailableNodesMetaData } from '../hooks'
import SubGraphChildren from './sub-graph-children'
type SubGraphMainProps = {
type SubGraphMainBaseProps = {
nodes: Node[]
edges: Edge[]
viewport: Viewport
agentName: string
title: string
extractorNodeId: string
configsMap?: HooksStoreShape['configsMap']
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
selectableNodeTypes?: BlockEnum[]
onSave?: (nodes: Node[], edges: Edge[]) => void
onSyncWorkflowDraft?: SyncWorkflowDraft
}
const SubGraphMain: FC<SubGraphMainProps> = ({
nodes,
edges,
viewport,
agentName,
extractorNodeId,
configsMap,
mentionConfig,
onMentionConfigChange,
onSave,
onSyncWorkflowDraft,
}) => {
type SubGraphMainProps
= | (SubGraphMainBaseProps & {
variant: 'agent'
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
})
| (SubGraphMainBaseProps & {
variant: 'assemble'
})
const SubGraphMain: FC<SubGraphMainProps> = (props) => {
const {
nodes,
edges,
viewport,
variant,
title,
extractorNodeId,
configsMap,
selectableNodeTypes,
onSave,
onSyncWorkflowDraft,
} = props
const reactFlowStore = useStoreApi()
const availableNodesMetaData = useAvailableNodesMetaData()
const flowType = configsMap?.flowType ?? FlowType.appFlow
@ -76,32 +87,53 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
}
}, [handleSyncSubGraphDraft, onSyncWorkflowDraft])
const resolvedSelectableTypes = useMemo(() => {
if (selectableNodeTypes && selectableNodeTypes.length > 0)
return selectableNodeTypes
return variant === 'agent' ? [BlockEnum.LLM] : [BlockEnum.Code]
}, [selectableNodeTypes, variant])
const hooksStore = useMemo(() => ({
interactionMode: 'subgraph',
subGraphSelectableNodeTypes: resolvedSelectableTypes,
availableNodesMetaData,
configsMap,
fetchInspectVars,
...inspectVarsCrud,
doSyncWorkflowDraft: handleSyncWorkflowDraft,
syncWorkflowDraftWhenPageClose: handleSyncSubGraphDraft,
}), [availableNodesMetaData, configsMap, fetchInspectVars, handleSyncSubGraphDraft, handleSyncWorkflowDraft, inspectVarsCrud])
}), [availableNodesMetaData, configsMap, fetchInspectVars, handleSyncSubGraphDraft, handleSyncWorkflowDraft, inspectVarsCrud, resolvedSelectableTypes])
const subGraphChildren = variant === 'agent'
? (
<SubGraphChildren
variant="agent"
title={title}
extractorNodeId={extractorNodeId}
mentionConfig={props.mentionConfig}
onMentionConfigChange={props.onMentionConfigChange}
/>
)
: (
<SubGraphChildren
variant="assemble"
title={title}
extractorNodeId={extractorNodeId}
/>
)
return (
<WorkflowWithInnerContext
nodes={nodes}
edges={edges}
viewport={viewport}
// eslint-disable-next-line ts/no-explicit-any -- TODO: remove after typing boundary
hooksStore={hooksStore as any}
allowSelectionWhenReadOnly
canvasReadOnly
interactionMode="subgraph"
>
<SubGraphChildren
agentName={agentName}
extractorNodeId={extractorNodeId}
mentionConfig={mentionConfig}
onMentionConfigChange={onMentionConfigChange}
/>
{subGraphChildren}
</WorkflowWithInnerContext>
)
}

View File

@ -7,6 +7,7 @@ import { memo, useEffect, useMemo } from 'react'
import WorkflowWithDefaultContext from '@/app/components/workflow'
import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION } from '@/app/components/workflow/constants'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
import SubGraphMain from './components/sub-graph-main'
@ -18,7 +19,7 @@ const SUB_GRAPH_ENTRY_POSITION = {
x: START_INITIAL_POSITION.x,
y: 150,
}
const SUB_GRAPH_LLM_POSITION = {
const SUB_GRAPH_EXTRACTOR_POSITION = {
x: SUB_GRAPH_ENTRY_POSITION.x + NODE_WIDTH_X_OFFSET - SUB_GRAPH_EDGE_GAP,
y: SUB_GRAPH_ENTRY_POSITION.y,
}
@ -33,19 +34,19 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
const {
toolNodeId,
paramKey,
agentName,
agentNodeId,
mentionConfig,
onMentionConfigChange,
extractorNode,
toolParamValue,
parentAvailableNodes,
parentAvailableVars,
configsMap,
selectableNodeTypes,
onSave,
onSyncWorkflowDraft,
} = props
const isAgentVariant = props.variant === 'agent'
const sourceTitle = isAgentVariant ? (props.agentName || '') : (props.title || '')
const resolvedAgentNodeId = isAgentVariant ? props.agentNodeId : ''
const setParentAvailableVars = useStore(state => state.setParentAvailableVars)
const setParentAvailableNodes = useStore(state => state.setParentAvailableNodes)
@ -55,28 +56,47 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
}, [parentAvailableNodes, parentAvailableVars, setParentAvailableNodes, setParentAvailableVars])
const promptText = useMemo(() => {
if (!toolParamValue)
if (!isAgentVariant || !toolParamValue)
return ''
// Reason: escape agent id before building a regex pattern.
const escapedAgentId = agentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const escapedAgentId = resolvedAgentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const leadingPattern = new RegExp(`^\\{\\{[@#]${escapedAgentId}\\.context[@#]\\}\\}`)
return toolParamValue.replace(leadingPattern, '')
}, [agentNodeId, toolParamValue])
}, [isAgentVariant, resolvedAgentNodeId, toolParamValue])
const startNode = useMemo(() => {
if (!isAgentVariant) {
return {
id: 'subgraph-source',
type: CUSTOM_SUB_GRAPH_START_NODE,
position: SUB_GRAPH_ENTRY_POSITION,
data: {
type: BlockEnum.Start,
title: sourceTitle,
desc: '',
selected: false,
iconType: 'assemble',
variables: [],
},
selected: false,
selectable: false,
draggable: false,
connectable: false,
focusable: false,
deletable: false,
}
}
return {
id: 'subgraph-source',
type: 'custom',
type: CUSTOM_SUB_GRAPH_START_NODE,
position: SUB_GRAPH_ENTRY_POSITION,
data: {
type: BlockEnum.Start,
title: agentName,
title: sourceTitle,
desc: '',
_connectedSourceHandleIds: ['source'],
_connectedTargetHandleIds: [],
_subGraphEntry: true,
_iconTypeOverride: BlockEnum.Agent,
selected: false,
iconType: 'agent',
variables: [],
},
selected: false,
@ -86,65 +106,83 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
focusable: false,
deletable: false,
}
}, [agentName])
}, [isAgentVariant, sourceTitle])
const extractorDisplayNode = useMemo(() => {
if (!extractorNode)
return null
if (isAgentVariant) {
const extractorNode = props.extractorNode
if (!extractorNode)
return null
const applyPromptText = (item: PromptItem) => {
if (item.edition_type === EditionType.jinja2) {
return {
...item,
text: promptText,
jinja2_text: promptText,
}
}
return { ...item, text: promptText }
}
const nextPromptTemplate = (() => {
const template = extractorNode.data.prompt_template
if (!Array.isArray(template))
return applyPromptText(template as PromptItem)
const userIndex = template.findIndex(
item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user,
)
if (userIndex >= 0) {
return template.map((item, index) => {
if (index !== userIndex)
return item
return applyPromptText(item as PromptItem)
}) as PromptTemplateItem[]
}
const useJinja = template.some(
item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2,
)
const defaultUserPrompt: PromptItem = useJinja
? {
role: PromptRole.user,
const applyPromptText = (item: PromptItem) => {
if (item.edition_type === EditionType.jinja2) {
return {
...item,
text: promptText,
jinja2_text: promptText,
edition_type: EditionType.jinja2,
}
: { role: PromptRole.user, text: promptText }
return [...template, defaultUserPrompt] as PromptTemplateItem[]
})()
}
return { ...item, text: promptText }
}
const nextPromptTemplate = (() => {
const template = extractorNode.data.prompt_template
if (!Array.isArray(template))
return applyPromptText(template as PromptItem)
const userIndex = template.findIndex(
item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user,
)
if (userIndex >= 0) {
return template.map((item, index) => {
if (index !== userIndex)
return item
return applyPromptText(item as PromptItem)
}) as PromptTemplateItem[]
}
const useJinja = template.some(
item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2,
)
const defaultUserPrompt: PromptItem = useJinja
? {
role: PromptRole.user,
text: promptText,
jinja2_text: promptText,
edition_type: EditionType.jinja2,
}
: { role: PromptRole.user, text: promptText }
return [...template, defaultUserPrompt] as PromptTemplateItem[]
})()
return {
...extractorNode,
hidden: false,
selected: false,
position: SUB_GRAPH_EXTRACTOR_POSITION,
data: {
...extractorNode.data,
selected: false,
prompt_template: nextPromptTemplate,
},
}
}
const extractorNode = props.extractorNode
if (!extractorNode)
return null
return {
...extractorNode,
hidden: false,
selected: false,
position: SUB_GRAPH_LLM_POSITION,
position: SUB_GRAPH_EXTRACTOR_POSITION,
data: {
...extractorNode.data,
selected: false,
prompt_template: nextPromptTemplate,
},
}
}, [extractorNode, promptText])
}, [isAgentVariant, promptText, props.extractorNode])
const nodesSource = useMemo(() => {
if (!extractorDisplayNode)
@ -168,30 +206,54 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
selectable: false,
data: {
sourceType: BlockEnum.Start,
targetType: BlockEnum.LLM,
targetType: isAgentVariant ? BlockEnum.LLM : BlockEnum.Code,
_isTemp: true,
_isSubGraphTemp: true,
},
},
]
}, [extractorDisplayNode, startNode])
}, [extractorDisplayNode, isAgentVariant, startNode])
const { nodes, edges } = useSubGraphNodes(nodesSource, edgesSource)
if (isAgentVariant) {
return (
<WorkflowWithDefaultContext
nodes={nodes}
edges={edges}
>
<SubGraphMain
variant="agent"
nodes={nodes}
edges={edges}
viewport={defaultViewport}
title={sourceTitle}
extractorNodeId={`${toolNodeId}_ext_${paramKey}`}
configsMap={configsMap}
mentionConfig={props.mentionConfig}
onMentionConfigChange={props.onMentionConfigChange}
selectableNodeTypes={selectableNodeTypes}
onSave={onSave}
onSyncWorkflowDraft={onSyncWorkflowDraft}
/>
</WorkflowWithDefaultContext>
)
}
return (
<WorkflowWithDefaultContext
nodes={nodes}
edges={edges}
>
<SubGraphMain
variant="assemble"
nodes={nodes}
edges={edges}
viewport={defaultViewport}
agentName={agentName}
title={sourceTitle}
extractorNodeId={`${toolNodeId}_ext_${paramKey}`}
configsMap={configsMap}
mentionConfig={mentionConfig}
onMentionConfigChange={onMentionConfigChange}
selectableNodeTypes={selectableNodeTypes}
onSave={onSave}
onSyncWorkflowDraft={onSyncWorkflowDraft}
/>

View File

@ -1,8 +1,9 @@
import type { StateCreator } from 'zustand'
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
import type { BlockEnum, Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
export type SyncWorkflowDraftCallback = {
onSuccess?: () => void
@ -15,23 +16,38 @@ export type SyncWorkflowDraft = (
callback?: SyncWorkflowDraftCallback,
) => Promise<void>
export type SubGraphProps = {
export type SubGraphVariant = 'agent' | 'assemble'
type BaseSubGraphProps = {
toolNodeId: string
paramKey: string
sourceVariable: ValueSelector
agentNodeId: string
agentName: string
configsMap?: HooksStoreShape['configsMap']
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
extractorNode?: Node<LLMNodeType>
toolParamValue?: string
parentAvailableNodes?: Node[]
parentAvailableVars?: NodeOutPutVar[]
selectableNodeTypes?: BlockEnum[]
onSave?: (nodes: Node[], edges: Edge[]) => void
onSyncWorkflowDraft?: SyncWorkflowDraft
}
export type AgentSubGraphProps = BaseSubGraphProps & {
variant: 'agent'
sourceVariable: ValueSelector
agentNodeId: string
agentName: string
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
extractorNode?: Node<LLMNodeType>
}
export type AssembleSubGraphProps = BaseSubGraphProps & {
variant: 'assemble'
title: string
extractorNode?: Node<CodeNodeType>
}
export type SubGraphProps = AgentSubGraphProps | AssembleSubGraphProps
export type SubGraphSliceShape = {
parentAvailableVars: NodeOutPutVar[]
parentAvailableNodes: Node[]