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

@ -0,0 +1,26 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.14286 5.14286V3.42857L8 5.71429L5.14286 8V6.28571H0V5.14286H5.14286ZM0.83303 7.42857H2.04658C2.72474 9.10389 4.36721 10.28571 6.28571 10.28571C8.81049 10.28571 10.85717 8.23903 10.85717 5.71429C10.85717 3.18956 8.81049 1.14285 6.28571 1.14285C4.36721 1.14285 2.72474 2.32467 2.04658 4H0.83303C1.56118 1.68165 3.72706 0 6.28571 0C9.4416 0 12 2.55837 12 5.71429C12 8.87014 9.4416 11.42854 6.28571 11.42854C3.72706 11.42854 1.56118 9.74691 0.83303 7.42857Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "AssembleVariablesAlt"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './AssembleVariablesAlt.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AssembleVariablesAlt'
export default Icon

View File

@ -1,4 +1,5 @@
export { default as AssembleVariables } from './AssembleVariables'
export { default as AssembleVariablesAlt } from './AssembleVariablesAlt'
export { default as AtSign } from './AtSign'
export { default as Bookmark } from './Bookmark'
export { default as Check } from './Check'

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[]

View File

@ -46,6 +46,7 @@ export type CommonHooksFnMap = {
handleWorkflowTriggerWebhookRunInWorkflow: (params: { nodeId: string }) => void
handleWorkflowTriggerPluginRunInWorkflow: (nodeId?: string) => void
handleWorkflowRunAllTriggersInWorkflow: (nodeIds: string[]) => void
subGraphSelectableNodeTypes?: BlockEnum[]
availableNodesMetaData?: AvailableNodesMetaData
getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string, traceUrl: string }
exportCheck?: () => Promise<void>
@ -93,6 +94,7 @@ export const createHooksStore = ({
handleWorkflowTriggerWebhookRunInWorkflow = noop,
handleWorkflowTriggerPluginRunInWorkflow = noop,
handleWorkflowRunAllTriggersInWorkflow = noop,
subGraphSelectableNodeTypes,
availableNodesMetaData = {
nodes: [],
},
@ -136,6 +138,7 @@ export const createHooksStore = ({
handleWorkflowTriggerWebhookRunInWorkflow,
handleWorkflowTriggerPluginRunInWorkflow,
handleWorkflowRunAllTriggersInWorkflow,
subGraphSelectableNodeTypes,
availableNodesMetaData,
getWorkflowRunAndTraceUrl,
exportCheck,

View File

@ -89,6 +89,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 CustomSubGraphStartNode from './nodes/sub-graph-start'
import { CUSTOM_SUB_GRAPH_START_NODE } from './nodes/sub-graph-start/constants'
import CustomNoteNode from './note-node'
import { CUSTOM_NOTE_NODE } from './note-node/constants'
import Operator from './operator'
@ -119,6 +121,7 @@ const nodeTypes = {
[CUSTOM_NODE]: CustomNode,
[CUSTOM_NOTE_NODE]: CustomNoteNode,
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
[CUSTOM_SUB_GRAPH_START_NODE]: CustomSubGraphStartNode,
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
[CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode,
@ -355,6 +358,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
const dataSourceList = useStore(s => s.dataSourceList)
// buildInTools, customTools, workflowTools, mcpTools, dataSourceList
const configsMap = useHooksStore(s => s.configsMap)
const subGraphSelectableNodeTypes = useHooksStore(s => s.subGraphSelectableNodeTypes)
const [isLoadedVars, setIsLoadedVars] = useState(false)
const [vars, setVars] = useState<VarInInspect[]>([])
useEffect(() => {
@ -393,12 +397,17 @@ export const Workflow: FC<WorkflowProps> = memo(({
const handleNodeClickInMode = useCallback<NodeMouseHandler>(
(event, node) => {
if (isSubGraph && node.data.type !== BlockEnum.LLM)
return
if (isSubGraph) {
const allowTypes = subGraphSelectableNodeTypes?.length
? subGraphSelectableNodeTypes
: [BlockEnum.LLM]
if (!allowTypes.includes(node.data.type))
return
}
handleNodeClick(event, node)
},
[handleNodeClick, isSubGraph],
[handleNodeClick, isSubGraph, subGraphSelectableNodeTypes],
)
return (

View File

@ -351,6 +351,25 @@ const VarReferenceVars: FC<Props> = ({
)
}
{
showAssembleVariables && (
<div className="flex items-center border-t border-divider-subtle pt-1">
<button
type="button"
className="flex h-6 w-full items-center rounded-md pl-3 pr-[18px] text-text-secondary hover:bg-state-base-hover"
onClick={handleAssembleVariables}
onMouseDown={e => e.preventDefault()}
>
<span className="mr-1 flex h-4 w-4 items-center justify-center rounded bg-util-colors-blue-blue-500">
<AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />
</span>
<span className="system-xs-medium truncate" title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}>
{t('nodes.tool.assembleVariables', { ns: 'workflow' })}
</span>
</button>
</div>
)
}
{filteredVars.length > 0
? (
<div className={cn('max-h-[85vh] overflow-y-auto', maxHeightClass)}>
@ -404,25 +423,6 @@ const VarReferenceVars: FC<Props> = ({
/>
)
}
{
showAssembleVariables && (
<div className="flex items-center border-t border-divider-subtle pt-1">
<button
type="button"
className="flex h-6 w-full items-center rounded-md pl-3 pr-[18px] text-text-secondary hover:bg-state-base-hover"
onClick={handleAssembleVariables}
onMouseDown={e => e.preventDefault()}
>
<span className="mr-1 flex h-4 w-4 items-center justify-center rounded bg-util-colors-blue-blue-500">
<AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />
</span>
<span className="system-xs-medium truncate" title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}>
{t('nodes.tool.assembleVariables', { ns: 'workflow' })}
</span>
</button>
</div>
)
}
</>
)
}

View File

@ -0,0 +1 @@
export const CUSTOM_SUB_GRAPH_START_NODE = 'custom-sub-graph-start'

View File

@ -0,0 +1,60 @@
import type { NodeProps } from 'reactflow'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import Tooltip from '@/app/components/base/tooltip'
import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle'
import { cn } from '@/utils/classnames'
type SubGraphStartNodeData = CommonNodeType<{
tooltip?: string
iconType?: string
}>
type IconComponent = typeof Agent
const iconMap: Record<string, IconComponent> = {
agent: Agent,
assemble: AssembleVariablesAlt,
}
const SubGraphStartNode = ({ id, data }: NodeProps<SubGraphStartNodeData>) => {
const { t } = useTranslation()
const iconType = data?.iconType || 'agent'
const Icon = iconMap[iconType] || Agent
const rawTitle = data?.title?.trim() || ''
const showTitle = iconType === 'agent' && !!rawTitle
const displayTitle = showTitle && (rawTitle.startsWith('@') ? rawTitle : `@${rawTitle}`)
const tooltip = data?.tooltip
|| (iconType === 'assemble' ? t('blocks.start', { ns: 'workflow' }) : (data?.title || t('blocks.start', { ns: 'workflow' })))
return (
<div
className={cn(
'nodrag group mt-1 flex h-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs',
showTitle ? 'gap-1.5 px-2' : 'w-11',
)}
>
<Tooltip popupContent={tooltip} asChild={false}>
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
<Icon className="h-3 w-3 text-text-primary-on-surface" />
</div>
</Tooltip>
{showTitle && (
<span className="system-xs-medium max-w-[160px] truncate text-text-secondary">
{displayTitle}
</span>
)}
<NodeSourceHandle
id={id}
data={data}
handleClassName="!top-1/2 !-right-[9px] !-translate-y-1/2"
handleId="source"
/>
</div>
)
}
export default memo(SubGraphStartNode)

View File

@ -2,6 +2,7 @@ import type { FC } from 'react'
import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { AssembleVariables } from '@/app/components/base/icons/src/vender/line/general'
import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import { cn } from '@/utils/classnames'
@ -34,8 +35,11 @@ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
: 'border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
>
<div className="flex h-4 w-4 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
<Agent className="h-3 w-3 text-text-primary-on-surface" />
<div className={cn('flex h-4 w-4 items-center justify-center rounded', showAtPrefix
? 'bg-util-colors-indigo-indigo-500'
: 'bg-util-colors-blue-blue-500')}
>
{showAtPrefix ? <Agent className="h-3 w-3 text-text-primary-on-surface" /> : <AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />}
</div>
<span className="system-xs-medium text-text-secondary">
{showAtPrefix && '@'}

View File

@ -36,9 +36,16 @@ import Placeholder from './placeholder'
/**
* Matches agent context variable syntax: {{@nodeId.context@}}
* Example: {{@agent-123.context@}} -> captures "agent-123"
* Example: {{@agent-123.context@}}
*/
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
const getAgentNodeIdFromContextVar = (placeholder: string) => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}
const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string) => {
if (!toolNodeId || !paramKey)
@ -309,8 +316,9 @@ const MixedVariableTextInput = ({
const matches = text.matchAll(AGENT_CONTEXT_VAR_PATTERN)
for (const match of matches) {
const variablePath = match[1]
const nodeId = variablePath.split('.')[0]
const nodeId = getAgentNodeIdFromContextVar(match[0])
if (!nodeId)
continue
const node = nodesByIdMap[nodeId]
if (node && contextNodeIds.has(nodeId)) {
return {
@ -461,8 +469,8 @@ const MixedVariableTextInput = ({
if (!agentNodeId || !onChange)
return
const valueWithoutAgentVars = value.replace(AGENT_CONTEXT_VAR_PATTERN, (match, variablePath) => {
const nodeId = variablePath.split('.')[0]
const valueWithoutAgentVars = value.replace(AGENT_CONTEXT_VAR_PATTERN, (match) => {
const nodeId = getAgentNodeIdFromContextVar(match)
return nodeId === agentNodeId ? '' : match
})
@ -552,6 +560,7 @@ const MixedVariableTextInput = ({
<AgentHeaderBar
agentName={t('nodes.tool.assembleVariables', { ns: 'workflow' })}
onRemove={handleAssembleRemove}
onViewInternals={handleOpenSubGraphModal}
hasWarning={hasAssembleWarning}
showAtPrefix={false}
/>
@ -599,10 +608,21 @@ const MixedVariableTextInput = ({
}}
/>
)}
{toolNodeId && detectedAgentFromValue && sourceVariable && (
{toolNodeId && paramKey && isAssembleValue && (
<SubGraphModal
isOpen={isSubGraphModalOpen}
onClose={handleCloseSubGraphModal}
variant="assemble"
toolNodeId={toolNodeId}
paramKey={paramKey}
title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}
/>
)}
{toolNodeId && paramKey && !isAssembleValue && detectedAgentFromValue && sourceVariable && (
<SubGraphModal
isOpen={isSubGraphModalOpen}
onClose={handleCloseSubGraphModal}
variant="agent"
toolNodeId={toolNodeId}
paramKey={paramKey}
sourceVariable={sourceVariable}

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { SubGraphModalProps } from './types'
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 { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
import type { Node, PromptItem, PromptTemplateItem } from '@/app/components/workflow/types'
@ -11,24 +12,29 @@ import { noop } from 'es-toolkit/function'
import { Fragment, memo, useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import { useIsChatMode, useNodesSyncDraft, useWorkflow, useWorkflowVariables } from '@/app/components/workflow/hooks'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
import SubGraphCanvas from './sub-graph-canvas'
const SubGraphModal: FC<SubGraphModalProps> = ({
isOpen,
onClose,
toolNodeId,
paramKey,
sourceVariable,
agentName,
agentNodeId,
}) => {
const SubGraphModal: FC<SubGraphModalProps> = (props) => {
const { t } = useTranslation()
const { isOpen, onClose, variant, toolNodeId, paramKey } = props
const isAgentVariant = variant === 'agent'
const resolvedAgentNodeId = isAgentVariant ? props.agentNodeId : ''
const agentName = isAgentVariant ? props.agentName : ''
const assembleTitle = !isAgentVariant ? props.title : ''
const modalTitle = useMemo(() => {
const baseTitle = isAgentVariant
? agentName
: (assembleTitle || t('nodes.tool.assembleVariables', { ns: 'workflow' }))
const prefix = isAgentVariant && baseTitle ? '@' : ''
return `${prefix}${baseTitle} ${t('subGraphModal.title', { ns: 'workflow' })}`.trim()
}, [agentName, assembleTitle, isAgentVariant, t])
const reactflowStore = useStoreApi()
const workflowNodes = useWorkflowStore(state => state.nodes)
const workflowEdges = useReactFlowStore(state => state.edges)
@ -41,13 +47,16 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
const extractorNode = useMemo(() => {
return workflowNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType> | undefined
return workflowNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType | CodeNodeType> | undefined
}, [extractorNodeId, workflowNodes])
const toolNode = useMemo(() => {
return workflowNodes.find(node => node.id === toolNodeId)
}, [toolNodeId, workflowNodes])
const toolParam = (toolNode?.data as ToolNodeType | undefined)?.tool_parameters?.[paramKey]
const toolParamValue = toolParam?.value as string | undefined
const assemblePlaceholder = useMemo(() => {
return `{{#${toolNodeId}_ext_${paramKey}.result#}}`
}, [paramKey, toolNodeId])
const parentBeforeNodes = useMemo(() => {
if (!isOpen)
@ -56,25 +65,28 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
}, [getBeforeNodesInSameBranch, isOpen, toolNodeId, workflowEdges, workflowNodes])
const parentContextNodes = useMemo(() => {
if (!parentBeforeNodes.length)
if (!parentBeforeNodes.length || !isAgentVariant)
return []
return parentBeforeNodes.filter(node => node.data.type === BlockEnum.Agent || node.data.type === BlockEnum.LLM)
}, [parentBeforeNodes])
}, [isAgentVariant, parentBeforeNodes])
const parentContextNodeIds = useMemo(() => {
return parentContextNodes.map(node => node.id)
}, [parentContextNodes])
const parentAvailableNodes = useMemo(() => {
if (!isOpen)
return []
return isAgentVariant ? parentContextNodes : parentBeforeNodes
}, [isAgentVariant, isOpen, parentBeforeNodes, parentContextNodes])
const parentAvailableVars = useMemo(() => {
if (!parentContextNodeIds.length)
if (!parentAvailableNodes.length)
return []
const vars = getNodeAvailableVars({
beforeNodes: parentContextNodes,
beforeNodes: parentAvailableNodes,
isChatMode,
filterVar: () => true,
})
return vars.filter(nodeVar => parentContextNodeIds.includes(nodeVar.nodeId))
}, [getNodeAvailableVars, isChatMode, parentContextNodeIds, parentContextNodes])
const availableNodeIds = new Set(parentAvailableNodes.map(node => node.id))
return vars.filter(nodeVar => availableNodeIds.has(nodeVar.nodeId))
}, [getNodeAvailableVars, isChatMode, parentAvailableNodes])
const mentionConfig = useMemo<MentionConfig>(() => {
const current = toolParam?.mention_config
@ -91,6 +103,9 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
}, [extractorNodeId, paramKey, toolParam?.mention_config])
const handleMentionConfigChange = useCallback((config: MentionConfig) => {
if (!isAgentVariant)
return
const { getNodes, setNodes } = reactflowStore.getState()
const nextNodes = getNodes().map((node) => {
if (node.id !== toolNodeId)
@ -118,10 +133,10 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
})
setNodes(nextNodes)
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft, paramKey, reactflowStore, toolNodeId])
}, [handleSyncWorkflowDraft, isAgentVariant, paramKey, reactflowStore, toolNodeId])
useEffect(() => {
if (!toolParam || (toolParam.type && toolParam.type !== VarKindType.mention))
if (!isAgentVariant || !toolParam || (toolParam.type && toolParam.type !== VarKindType.mention))
return
const current = toolParam.mention_config
@ -132,7 +147,7 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
if (needsExtractor || needsNullStrategy || needsOutputSelector || needsDefaultValue)
handleMentionConfigChange(mentionConfig)
}, [handleMentionConfigChange, mentionConfig, toolParam])
}, [handleMentionConfigChange, isAgentVariant, mentionConfig, toolParam])
const getUserPromptText = useCallback((promptTemplate?: PromptTemplateItem[] | PromptItem) => {
if (!promptTemplate)
@ -156,23 +171,46 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
// TODO: handle external workflow updates while sub-graph modal is open.
const handleSave = useCallback((subGraphNodes: Node[]) => {
const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType> | undefined
const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType | CodeNodeType> | undefined
if (!extractorNodeData)
return
const userPromptText = getUserPromptText(extractorNodeData.data?.prompt_template)
const placeholder = `{{@${agentNodeId}.context@}}`
const nextValue = `${placeholder}${userPromptText}`
const ensureAssembleOutputs = (payload: CodeNodeType) => {
const outputs = payload.outputs || {}
if (outputs.result)
return payload
return {
...payload,
outputs: {
...outputs,
result: {
type: VarType.string,
children: null,
},
},
}
}
const userPromptText = isAgentVariant
? getUserPromptText((extractorNodeData.data as LLMNodeType).prompt_template)
: ''
const placeholder = isAgentVariant && resolvedAgentNodeId ? `{{@${resolvedAgentNodeId}.context@}}` : ''
const nextValue = isAgentVariant
? `${placeholder}${userPromptText}`
: assemblePlaceholder
const { getNodes, setNodes } = reactflowStore.getState()
const nextNodes = getNodes().map((node) => {
if (node.id === extractorNodeId) {
const nextData = isAgentVariant
? extractorNodeData.data
: ensureAssembleOutputs(extractorNodeData.data as CodeNodeType)
return {
...node,
hidden: true,
data: {
...node.data,
...extractorNodeData.data,
...nextData,
parent_node_id: toolNodeId,
},
}
@ -200,7 +238,7 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
})
setNodes(nextNodes)
setControlPromptEditorRerenderKey(Date.now())
}, [agentNodeId, extractorNodeId, getUserPromptText, paramKey, reactflowStore, setControlPromptEditorRerenderKey, toolNodeId])
}, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, toolNodeId])
return (
<Transition appear show={isOpen} as={Fragment}>
@ -215,13 +253,12 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-subtle px-4">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
<Agent className="h-4 w-4 text-text-primary-on-surface" />
{isAgentVariant
? <Agent className="h-4 w-4 text-text-primary-on-surface" />
: <AssembleVariablesAlt className="h-4 w-4 text-text-primary-on-surface" />}
</div>
<span className="system-md-semibold text-text-primary">
@
{agentName}
{' '}
{t('subGraphModal.title', { ns: 'workflow' })}
{modalTitle}
</span>
</div>
<button
@ -234,22 +271,41 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
</div>
<div className="bg-workflow-canvas-wrapper relative flex-1 overflow-hidden">
<SubGraphCanvas
toolNodeId={toolNodeId}
paramKey={paramKey}
sourceVariable={sourceVariable}
agentNodeId={agentNodeId}
agentName={agentName}
configsMap={configsMap}
mentionConfig={mentionConfig}
onMentionConfigChange={handleMentionConfigChange}
extractorNode={extractorNode}
toolParamValue={toolParamValue}
parentAvailableNodes={parentContextNodes}
parentAvailableVars={parentAvailableVars}
onSave={handleSave}
onSyncWorkflowDraft={doSyncWorkflowDraft}
/>
{variant === 'agent'
? (
<SubGraphCanvas
variant="agent"
toolNodeId={toolNodeId}
paramKey={paramKey}
sourceVariable={props.sourceVariable}
agentNodeId={props.agentNodeId}
agentName={props.agentName}
configsMap={configsMap}
mentionConfig={mentionConfig}
onMentionConfigChange={handleMentionConfigChange}
extractorNode={extractorNode as Node<LLMNodeType> | undefined}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAvailableNodes}
parentAvailableVars={parentAvailableVars}
onSave={handleSave}
onSyncWorkflowDraft={doSyncWorkflowDraft}
/>
)
: (
<SubGraphCanvas
variant="assemble"
toolNodeId={toolNodeId}
paramKey={paramKey}
title={props.title}
configsMap={configsMap}
extractorNode={extractorNode as Node<CodeNodeType> | undefined}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAvailableNodes}
parentAvailableVars={parentAvailableVars}
onSave={handleSave}
onSyncWorkflowDraft={doSyncWorkflowDraft}
/>
)}
</div>
</DialogPanel>
</TransitionChild>

View File

@ -4,40 +4,10 @@ import type { SubGraphCanvasProps } from './types'
import { memo } from 'react'
import SubGraph from '@/app/components/sub-graph'
const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
toolNodeId,
paramKey,
sourceVariable,
agentNodeId,
agentName,
configsMap,
mentionConfig,
onMentionConfigChange,
extractorNode,
toolParamValue,
parentAvailableNodes,
parentAvailableVars,
onSave,
onSyncWorkflowDraft,
}) => {
const SubGraphCanvas: FC<SubGraphCanvasProps> = (props) => {
return (
<div className="h-full w-full">
<SubGraph
toolNodeId={toolNodeId}
paramKey={paramKey}
sourceVariable={sourceVariable}
agentNodeId={agentNodeId}
agentName={agentName}
configsMap={configsMap}
mentionConfig={mentionConfig}
onMentionConfigChange={onMentionConfigChange}
extractorNode={extractorNode}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAvailableNodes}
parentAvailableVars={parentAvailableVars}
onSave={onSave}
onSyncWorkflowDraft={onSyncWorkflowDraft}
/>
<SubGraph {...props} />
</div>
)
}

View File

@ -1,34 +1,25 @@
import type { SyncWorkflowDraft } from '@/app/components/sub-graph/types'
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { NodeOutPutVar, Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types'
import type { SubGraphProps } from '@/app/components/sub-graph/types'
import type { ValueSelector } from '@/app/components/workflow/types'
type WorkflowValueSelector = string[]
export type SubGraphModalProps = {
type BaseSubGraphModalProps = {
isOpen: boolean
onClose: () => void
toolNodeId: string
paramKey: string
sourceVariable: WorkflowValueSelector
}
type AgentSubGraphModalProps = BaseSubGraphModalProps & {
variant: 'agent'
sourceVariable: ValueSelector
agentName: string
agentNodeId: string
}
export type SubGraphCanvasProps = {
toolNodeId: string
paramKey: string
sourceVariable: WorkflowValueSelector
agentNodeId: string
agentName: string
configsMap?: HooksStoreShape['configsMap']
mentionConfig: MentionConfig
onMentionConfigChange: (config: MentionConfig) => void
extractorNode?: WorkflowNode<LLMNodeType>
toolParamValue?: string
parentAvailableNodes?: WorkflowNode[]
parentAvailableVars?: NodeOutPutVar[]
onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void
onSyncWorkflowDraft?: SyncWorkflowDraft
type AssembleSubGraphModalProps = BaseSubGraphModalProps & {
variant: 'assemble'
title: string
}
export type SubGraphModalProps = AgentSubGraphModalProps | AssembleSubGraphModalProps
export type SubGraphCanvasProps = SubGraphProps

View File

@ -20,7 +20,14 @@ import { useStrategyProviders } from '@/service/use-strategy'
import { cn } from '@/utils/classnames'
import { VarType } from './types'
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
const getAgentNodeIdFromContextVar = (placeholder: string) => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}
type AgentCheckValidContext = {
provider?: StrategyPluginDetail
strategy?: StrategyDetail
@ -80,7 +87,7 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
return
const matches = value.matchAll(AGENT_CONTEXT_VAR_PATTERN)
for (const match of matches) {
const agentNodeId = match[1]
const agentNodeId = getAgentNodeIdFromContextVar(match[0])
if (!agentNodeId)
continue
const entryKey = `${paramKey}:${agentNodeId}`

View File

@ -13,6 +13,7 @@ import {
} from '@/app/components/workflow/constants'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants'
import {
BlockEnum,
} from '@/app/components/workflow/types'
@ -442,6 +443,7 @@ const normaliseChildLayout = (
const startNode = nodes.find(node =>
node.type === CUSTOM_ITERATION_START_NODE
|| node.type === CUSTOM_LOOP_START_NODE
|| node.type === CUSTOM_SUB_GRAPH_START_NODE
|| node.data?.type === BlockEnum.LoopStart
|| node.data?.type === BlockEnum.IterationStart,
)

View File

@ -0,0 +1,60 @@
import type { NodeProps } from 'reactflow'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import Tooltip from '@/app/components/base/tooltip'
import { cn } from '@/utils/classnames'
import { NodeSourceHandle } from '../../node-handle'
type SubGraphStartNodeData = CommonNodeType<{
tooltip?: string
iconType?: string
}>
type IconComponent = typeof Agent
const iconMap: Record<string, IconComponent> = {
agent: Agent,
assemble: AssembleVariablesAlt,
}
const SubGraphStartNode = ({ id, data }: NodeProps<SubGraphStartNodeData>) => {
const { t } = useTranslation()
const iconType = data?.iconType || 'agent'
const Icon = iconMap[iconType] || Agent
const rawTitle = data?.title?.trim() || ''
const showTitle = iconType === 'agent' && !!rawTitle
const displayTitle = showTitle && (rawTitle.startsWith('@') ? rawTitle : `@${rawTitle}`)
const tooltip = data?.tooltip
|| (iconType === 'assemble' ? t('blocks.start', { ns: 'workflow' }) : (data?.title || t('blocks.start', { ns: 'workflow' })))
return (
<div
className={cn(
'nodrag group mt-1 flex h-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs',
showTitle ? 'gap-1.5 px-2' : 'w-11',
)}
>
<Tooltip popupContent={tooltip} asChild={false}>
<div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500">
<Icon className="h-3 w-3 text-text-primary-on-surface" />
</div>
</Tooltip>
{showTitle && (
<span className="system-xs-medium max-w-[160px] truncate text-text-secondary">
{displayTitle}
</span>
)}
<NodeSourceHandle
id={id}
data={data}
handleClassName="!top-1/2 !-right-[9px] !-translate-y-1/2"
handleId="source"
/>
</div>
)
}
export default memo(SubGraphStartNode)

View File

@ -29,6 +29,7 @@ import {
import CustomConnectionLine from '@/app/components/workflow/custom-connection-line'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants'
import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants'
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
import {
@ -40,6 +41,7 @@ import CustomEdge from './components/custom-edge'
import CustomNode from './components/nodes'
import IterationStartNode from './components/nodes/iteration-start'
import LoopStartNode from './components/nodes/loop-start'
import SubGraphStartNode from './components/nodes/sub-graph-start'
import CustomNoteNode from './components/note-node'
import ZoomInOut from './components/zoom-in-out'
import 'reactflow/dist/style.css'
@ -49,6 +51,7 @@ const nodeTypes = {
[CUSTOM_NODE]: CustomNode,
[CUSTOM_NOTE_NODE]: CustomNoteNode,
[CUSTOM_SIMPLE_NODE]: CustomNode,
[CUSTOM_SUB_GRAPH_START_NODE]: SubGraphStartNode,
[CUSTOM_ITERATION_START_NODE]: IterationStartNode,
[CUSTOM_LOOP_START_NODE]: LoopStartNode,
}