mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 16:08:04 +08:00
feat: sub-graph to use dynamic node generation
This commit is contained in:
@ -4,6 +4,7 @@ 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'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type SubGraphChildrenProps = {
|
||||
toolNodeId: string
|
||||
@ -20,7 +21,7 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
const nodes = s.getNodes()
|
||||
const currentNode = nodes.find(node => node.data.selected)
|
||||
|
||||
if (currentNode) {
|
||||
if (currentNode?.data.type === BlockEnum.LLM) {
|
||||
return {
|
||||
id: currentNode.id,
|
||||
type: currentNode.type,
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
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 { useMemo } from 'react'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { useAvailableNodesMetaData, useSubGraphPersistence } from '../hooks'
|
||||
import SubGraphChildren from './sub-graph-children'
|
||||
@ -23,37 +22,14 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
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: 'default',
|
||||
}
|
||||
|
||||
saveSubGraphData({
|
||||
nodes: updatedNodes,
|
||||
edges,
|
||||
config: existingData?.config || defaultConfig,
|
||||
})
|
||||
}, [edges, loadSubGraphData, saveSubGraphData])
|
||||
const { updateSubGraphConfig } = useSubGraphPersistence({ toolNodeId, paramKey })
|
||||
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
interactionMode: 'subgraph',
|
||||
availableNodesMetaData,
|
||||
doSyncWorkflowDraft: async () => {
|
||||
handleNodesChange(nodes)
|
||||
},
|
||||
syncWorkflowDraftWhenPageClose: () => {
|
||||
handleNodesChange(nodes)
|
||||
},
|
||||
doSyncWorkflowDraft: async () => {},
|
||||
syncWorkflowDraftWhenPageClose: () => {},
|
||||
handleRefreshWorkflowDraft: () => {},
|
||||
handleBackupDraft: () => {},
|
||||
handleLoadBackupDraft: () => {},
|
||||
@ -86,7 +62,7 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
resetConversationVar: async () => {},
|
||||
invalidateConversationVarValues: () => {},
|
||||
}
|
||||
}, [availableNodesMetaData, handleNodesChange, nodes])
|
||||
}, [availableNodesMetaData])
|
||||
|
||||
return (
|
||||
<WorkflowWithInnerContext
|
||||
@ -94,6 +70,9 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
hooksStore={hooksStore as any}
|
||||
allowSelectionWhenReadOnly
|
||||
canvasReadOnly
|
||||
interactionMode="subgraph"
|
||||
>
|
||||
<SubGraphChildren
|
||||
toolNodeId={toolNodeId}
|
||||
|
||||
@ -2,11 +2,13 @@ import type { FC } from 'react'
|
||||
import type { Viewport } from 'reactflow'
|
||||
import type { SubGraphProps } from './types'
|
||||
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
|
||||
import type { PromptItem } from '@/app/components/workflow/types'
|
||||
import { memo, useMemo } from 'react'
|
||||
import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { BlockEnum, PromptRole } from '@/app/components/workflow/types'
|
||||
import SubGraphMain from './components/sub-graph-main'
|
||||
import { useSubGraphInit, useSubGraphNodes, useSubGraphPersistence } from './hooks'
|
||||
import { useSubGraphNodes } from './hooks'
|
||||
import { createSubGraphSlice } from './store'
|
||||
|
||||
const defaultViewport: Viewport = {
|
||||
@ -16,15 +18,106 @@ const defaultViewport: Viewport = {
|
||||
}
|
||||
|
||||
const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
const { toolNodeId, paramKey } = props
|
||||
const {
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
agentName,
|
||||
agentNodeId,
|
||||
extractorNode,
|
||||
toolParamValue,
|
||||
} = props
|
||||
|
||||
const { loadSubGraphData } = useSubGraphPersistence({ toolNodeId, paramKey })
|
||||
const savedData = useMemo(() => loadSubGraphData(), [loadSubGraphData])
|
||||
const promptText = useMemo(() => {
|
||||
if (!toolParamValue)
|
||||
return ''
|
||||
// Reason: escape agent id before building a regex pattern.
|
||||
const escapedAgentId = agentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const leadingPattern = new RegExp(`^\\{\\{[@#]${escapedAgentId}\\.context[@#]\\}\\}`)
|
||||
return toolParamValue.replace(leadingPattern, '')
|
||||
}, [agentNodeId, toolParamValue])
|
||||
|
||||
const { initialNodes, initialEdges } = useSubGraphInit(props)
|
||||
const startNode = useMemo(() => {
|
||||
return {
|
||||
id: 'subgraph-source',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 150 },
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: agentName,
|
||||
desc: '',
|
||||
_connectedSourceHandleIds: ['source'],
|
||||
_connectedTargetHandleIds: [],
|
||||
variables: [],
|
||||
},
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
focusable: false,
|
||||
deletable: false,
|
||||
}
|
||||
}, [agentName])
|
||||
|
||||
const nodesSource = savedData?.nodes || initialNodes
|
||||
const edgesSource = savedData?.edges || initialEdges
|
||||
const extractorDisplayNode = useMemo(() => {
|
||||
if (!extractorNode)
|
||||
return null
|
||||
|
||||
const nextPromptTemplate = Array.isArray(extractorNode.data.prompt_template)
|
||||
? extractorNode.data.prompt_template.map((item: PromptItem) => {
|
||||
if (item.role === PromptRole.system)
|
||||
return { ...item, text: promptText }
|
||||
return item
|
||||
})
|
||||
: {
|
||||
...extractorNode.data.prompt_template,
|
||||
text: promptText,
|
||||
}
|
||||
|
||||
const hasSystemPrompt = Array.isArray(nextPromptTemplate)
|
||||
&& nextPromptTemplate.some((item: PromptItem) => item.role === PromptRole.system)
|
||||
const normalizedPromptTemplate = Array.isArray(nextPromptTemplate)
|
||||
? (hasSystemPrompt ? nextPromptTemplate : [{ role: PromptRole.system, text: promptText }, ...nextPromptTemplate])
|
||||
: nextPromptTemplate
|
||||
|
||||
return {
|
||||
...extractorNode,
|
||||
hidden: false,
|
||||
position: { x: 450, y: 150 },
|
||||
data: {
|
||||
...extractorNode.data,
|
||||
prompt_template: normalizedPromptTemplate,
|
||||
},
|
||||
}
|
||||
}, [extractorNode, promptText])
|
||||
|
||||
const nodesSource = useMemo(() => {
|
||||
if (!extractorDisplayNode)
|
||||
return [startNode]
|
||||
|
||||
return [startNode, extractorDisplayNode]
|
||||
}, [extractorDisplayNode, startNode])
|
||||
|
||||
const edgesSource = useMemo(() => {
|
||||
if (!extractorDisplayNode)
|
||||
return []
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${startNode.id}-${extractorDisplayNode.id}`,
|
||||
source: startNode.id,
|
||||
sourceHandle: 'source',
|
||||
target: extractorDisplayNode.id,
|
||||
targetHandle: 'target',
|
||||
type: 'custom',
|
||||
selectable: false,
|
||||
data: {
|
||||
sourceType: BlockEnum.Start,
|
||||
targetType: BlockEnum.LLM,
|
||||
_isTemp: true,
|
||||
_isSubGraphTemp: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}, [extractorDisplayNode, startNode])
|
||||
|
||||
const { nodes, edges } = useSubGraphNodes(nodesSource, edgesSource)
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ const initialState: Omit<SubGraphSliceShape, 'setSubGraphContext' | 'setSubGraph
|
||||
parameterKey: '',
|
||||
sourceAgentNodeId: '',
|
||||
sourceVariable: [],
|
||||
subGraphReadOnly: true,
|
||||
|
||||
subGraphNodes: [],
|
||||
subGraphEdges: [],
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { Edge, Node, NodeOutPutVar, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export type WhenOutputNoneOption = 'error' | 'default'
|
||||
@ -25,6 +26,9 @@ export type SubGraphProps = {
|
||||
sourceVariable: ValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
extractorNode?: Node<LLMNodeType>
|
||||
toolParamValue?: string
|
||||
onSave?: (nodes: Node[], edges: Edge[]) => void
|
||||
}
|
||||
|
||||
export type SubGraphSliceShape = {
|
||||
@ -32,6 +36,7 @@ export type SubGraphSliceShape = {
|
||||
parameterKey: string
|
||||
sourceAgentNodeId: string
|
||||
sourceVariable: ValueSelector
|
||||
subGraphReadOnly: boolean
|
||||
|
||||
subGraphNodes: Node[]
|
||||
subGraphEdges: Edge[]
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
} from './hooks'
|
||||
import { useHooksStore } from './hooks-store'
|
||||
import { BlockEnum, NodeRunningStatus } from './types'
|
||||
import { getEdgeColor } from './utils'
|
||||
|
||||
@ -56,6 +57,8 @@ const CustomEdge = ({
|
||||
})
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const interactionMode = useHooksStore(s => s.interactionMode)
|
||||
const allowGraphActions = interactionMode !== 'subgraph'
|
||||
const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration || (data as Edge['data'])?.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration || (data as Edge['data'])?.isInLoop)
|
||||
const {
|
||||
@ -136,35 +139,37 @@ const CustomEdge = ({
|
||||
stroke,
|
||||
strokeWidth: 2,
|
||||
opacity: data._dimmed ? 0.3 : (data._waitingRun ? 0.7 : 1),
|
||||
strokeDasharray: (data._isTemp && data.sourceType !== BlockEnum.Group && data.targetType !== BlockEnum.Group) ? '8 8' : undefined,
|
||||
strokeDasharray: (data._isTemp && !data._isSubGraphTemp && data.sourceType !== BlockEnum.Group && data.targetType !== BlockEnum.Group) ? '8 8' : undefined,
|
||||
}}
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag hover:scale-125',
|
||||
data?._hovering ? 'block' : 'hidden',
|
||||
open && '!block',
|
||||
data.isInIteration && `z-[${ITERATION_CHILDREN_Z_INDEX}]`,
|
||||
data.isInLoop && `z-[${LOOP_CHILDREN_Z_INDEX}]`,
|
||||
)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
opacity: data._waitingRun ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks)}
|
||||
triggerClassName={() => 'hover:scale-150 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
{allowGraphActions && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag hover:scale-125',
|
||||
data?._hovering ? 'block' : 'hidden',
|
||||
open && '!block',
|
||||
data.isInIteration && `z-[${ITERATION_CHILDREN_Z_INDEX}]`,
|
||||
data.isInLoop && `z-[${LOOP_CHILDREN_Z_INDEX}]`,
|
||||
)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
opacity: data._waitingRun ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks)}
|
||||
triggerClassName={() => 'hover:scale-150 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ export type AvailableNodesMetaData = {
|
||||
nodesMap?: Record<BlockEnum, NodeDefault<any>>
|
||||
}
|
||||
export type CommonHooksFnMap = {
|
||||
interactionMode?: 'default' | 'subgraph'
|
||||
doSyncWorkflowDraft: (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
@ -76,6 +77,7 @@ export type Shape = {
|
||||
} & CommonHooksFnMap
|
||||
|
||||
export const createHooksStore = ({
|
||||
interactionMode = 'default',
|
||||
doSyncWorkflowDraft = async () => noop(),
|
||||
syncWorkflowDraftWhenPageClose = noop,
|
||||
handleRefreshWorkflowDraft = noop,
|
||||
@ -118,6 +120,7 @@ export const createHooksStore = ({
|
||||
}: Partial<Shape>) => {
|
||||
return createStore<Shape>(set => ({
|
||||
refreshAll: props => set(state => ({ ...state, ...props })),
|
||||
interactionMode,
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
handleRefreshWorkflowDraft,
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
} from '../utils'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
|
||||
export const useShortcuts = (): void => {
|
||||
export const useShortcuts = (enabled = true): void => {
|
||||
const {
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
@ -66,13 +66,17 @@ export const useShortcuts = (): void => {
|
||||
}
|
||||
|
||||
const shouldHandleShortcut = useCallback((e: KeyboardEvent) => {
|
||||
if (!enabled)
|
||||
return false
|
||||
return !isEventTargetInputArea(e.target as HTMLElement)
|
||||
}, [])
|
||||
}, [enabled])
|
||||
|
||||
const shouldHandleCopy = useCallback(() => {
|
||||
if (!enabled)
|
||||
return false
|
||||
const selection = document.getSelection()
|
||||
return !selection || selection.isCollapsed
|
||||
}, [])
|
||||
}, [enabled])
|
||||
|
||||
useKeyPress(['delete', 'backspace'], (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
@ -282,6 +286,8 @@ export const useShortcuts = (): void => {
|
||||
|
||||
// Listen for zen toggle event from /zen command
|
||||
useEffect(() => {
|
||||
if (!enabled)
|
||||
return
|
||||
const handleZenToggle = () => {
|
||||
handleToggleMaximizeCanvas()
|
||||
}
|
||||
@ -290,5 +296,5 @@ export const useShortcuts = (): void => {
|
||||
return () => {
|
||||
window.removeEventListener(ZEN_TOGGLE_EVENT, handleZenToggle)
|
||||
}
|
||||
}, [handleToggleMaximizeCanvas])
|
||||
}, [enabled, handleToggleMaximizeCanvas])
|
||||
}
|
||||
|
||||
@ -498,13 +498,9 @@ export const useNodesReadOnly = () => {
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
|
||||
const getNodesReadOnly = useCallback((): boolean => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
historyWorkflowData,
|
||||
isRestoring,
|
||||
} = workflowStore.getState()
|
||||
const state = workflowStore.getState()
|
||||
|
||||
return !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring)
|
||||
return !!(state.workflowRunningData?.result.status === WorkflowRunningStatus.Running || state.historyWorkflowData || state.isRestoring)
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
NodeMouseHandler,
|
||||
Viewport,
|
||||
} from 'reactflow'
|
||||
import type { Shape as HooksStoreShape } from './hooks-store'
|
||||
@ -102,6 +103,7 @@ import {
|
||||
} from './store'
|
||||
import SyncingDataModal from './syncing-data-modal'
|
||||
import {
|
||||
BlockEnum,
|
||||
ControlMode,
|
||||
} from './types'
|
||||
import { setupScrollToNodeListener } from './utils/node-navigation'
|
||||
@ -134,6 +136,9 @@ export type WorkflowProps = {
|
||||
viewport?: Viewport
|
||||
children?: React.ReactNode
|
||||
onWorkflowDataUpdate?: (v: any) => void
|
||||
allowSelectionWhenReadOnly?: boolean
|
||||
canvasReadOnly?: boolean
|
||||
interactionMode?: 'default' | 'subgraph'
|
||||
}
|
||||
export const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodes: originalNodes,
|
||||
@ -141,6 +146,9 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
viewport,
|
||||
children,
|
||||
onWorkflowDataUpdate,
|
||||
allowSelectionWhenReadOnly = false,
|
||||
canvasReadOnly = false,
|
||||
interactionMode = 'default',
|
||||
}) => {
|
||||
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
||||
const workflowStore = useWorkflowStore()
|
||||
@ -196,7 +204,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
if (!isEqual(oldData, nodesData)) {
|
||||
setNodesInStore(nodes)
|
||||
}
|
||||
}, [setNodesInStore, workflowStore])
|
||||
}, [setNodesInStore])
|
||||
useEffect(() => {
|
||||
setNodesOnlyChangeWithData(currentNodes as Node[])
|
||||
}, [currentNodes, setNodesOnlyChangeWithData])
|
||||
@ -328,7 +336,8 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
},
|
||||
})
|
||||
|
||||
useShortcuts()
|
||||
const isSubGraph = interactionMode === 'subgraph'
|
||||
useShortcuts(!isSubGraph)
|
||||
// Initialize workflow node search functionality
|
||||
useWorkflowSearch()
|
||||
|
||||
@ -382,6 +391,16 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeClickInMode = useCallback<NodeMouseHandler>(
|
||||
(event, node) => {
|
||||
if (isSubGraph && node.data.type !== BlockEnum.LLM)
|
||||
return
|
||||
|
||||
handleNodeClick(event, node)
|
||||
},
|
||||
[handleNodeClick, isSubGraph],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="workflow-container"
|
||||
@ -393,18 +412,18 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
ref={workflowContainerRef}
|
||||
>
|
||||
<SyncingDataModal />
|
||||
<CandidateNode />
|
||||
{!isSubGraph && <CandidateNode />}
|
||||
<div
|
||||
className="pointer-events-none absolute left-0 top-0 z-10 flex w-12 items-center justify-center p-1 pl-2"
|
||||
style={{ height: controlHeight }}
|
||||
>
|
||||
<Control />
|
||||
{!isSubGraph && <Control />}
|
||||
</div>
|
||||
<Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
|
||||
<PanelContextmenu />
|
||||
<NodeContextmenu />
|
||||
<SelectionContextmenu />
|
||||
<HelpLine />
|
||||
{!isSubGraph && <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />}
|
||||
{!isSubGraph && <PanelContextmenu />}
|
||||
{!isSubGraph && <NodeContextmenu />}
|
||||
{!isSubGraph && <SelectionContextmenu />}
|
||||
{!isSubGraph && <HelpLine />}
|
||||
{
|
||||
!!showConfirm && (
|
||||
<Confirm
|
||||
@ -427,38 +446,38 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
onNodeDragStop={handleNodeDragStop}
|
||||
onNodeMouseEnter={handleNodeEnter}
|
||||
onNodeMouseLeave={handleNodeLeave}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onConnect={handleNodeConnect}
|
||||
onConnectStart={handleNodeConnectStart}
|
||||
onConnectEnd={handleNodeConnectEnd}
|
||||
onNodeClick={handleNodeClickInMode}
|
||||
onNodeContextMenu={isSubGraph ? undefined : handleNodeContextMenu}
|
||||
onConnect={isSubGraph ? undefined : handleNodeConnect}
|
||||
onConnectStart={isSubGraph ? undefined : handleNodeConnectStart}
|
||||
onConnectEnd={isSubGraph ? undefined : handleNodeConnectEnd}
|
||||
onEdgeMouseEnter={handleEdgeEnter}
|
||||
onEdgeMouseLeave={handleEdgeLeave}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onSelectionStart={handleSelectionStart}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onSelectionDrag={handleSelectionDrag}
|
||||
onPaneContextMenu={handlePaneContextMenu}
|
||||
onSelectionContextMenu={handleSelectionContextMenu}
|
||||
onSelectionStart={isSubGraph ? undefined : handleSelectionStart}
|
||||
onSelectionChange={isSubGraph ? undefined : handleSelectionChange}
|
||||
onSelectionDrag={isSubGraph ? undefined : handleSelectionDrag}
|
||||
onPaneContextMenu={isSubGraph ? undefined : handlePaneContextMenu}
|
||||
onSelectionContextMenu={isSubGraph ? undefined : handleSelectionContextMenu}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
// NOTE: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same?
|
||||
connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }}
|
||||
defaultViewport={viewport}
|
||||
multiSelectionKeyCode={null}
|
||||
deleteKeyCode={null}
|
||||
nodesDraggable={!nodesReadOnly}
|
||||
nodesConnectable={!nodesReadOnly}
|
||||
nodesFocusable={!nodesReadOnly}
|
||||
edgesFocusable={!nodesReadOnly}
|
||||
panOnScroll={controlMode === ControlMode.Pointer && !workflowReadOnly}
|
||||
panOnDrag={controlMode === ControlMode.Hand || [1]}
|
||||
zoomOnPinch={true}
|
||||
zoomOnScroll={true}
|
||||
zoomOnDoubleClick={true}
|
||||
nodesDraggable={!(nodesReadOnly || canvasReadOnly || isSubGraph)}
|
||||
nodesConnectable={!(nodesReadOnly || canvasReadOnly || isSubGraph)}
|
||||
nodesFocusable={allowSelectionWhenReadOnly ? true : !nodesReadOnly}
|
||||
edgesFocusable={isSubGraph ? false : (allowSelectionWhenReadOnly ? true : !nodesReadOnly)}
|
||||
panOnScroll={!isSubGraph && controlMode === ControlMode.Pointer && !workflowReadOnly}
|
||||
panOnDrag={!isSubGraph && (controlMode === ControlMode.Hand || [1])}
|
||||
selectionOnDrag={!isSubGraph && controlMode === ControlMode.Pointer && !workflowReadOnly && !canvasReadOnly}
|
||||
zoomOnPinch={!isSubGraph}
|
||||
zoomOnScroll={!isSubGraph}
|
||||
zoomOnDoubleClick={!isSubGraph}
|
||||
isValidConnection={isValidConnection}
|
||||
selectionKeyCode={null}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly}
|
||||
minZoom={0.25}
|
||||
>
|
||||
<Background
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
Stop,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodesInteractions,
|
||||
@ -30,12 +31,18 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const interactionMode = useHooksStore(s => s.interactionMode)
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}, [])
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
const allowNodeMenu = interactionMode !== 'subgraph'
|
||||
const canSingleRun = canRunBySingle(data.type, isChildNode)
|
||||
|
||||
if (!allowNodeMenu && !canSingleRun)
|
||||
return null
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
@ -50,7 +57,7 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
canSingleRun && (
|
||||
<div
|
||||
className={`flex h-5 w-5 items-center justify-center rounded-md ${isSingleRunning && 'cursor-pointer hover:bg-state-base-hover'}`}
|
||||
onClick={() => {
|
||||
@ -80,13 +87,15 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<PanelOperator
|
||||
id={id}
|
||||
data={data}
|
||||
offset={0}
|
||||
onOpenChange={handleOpenChange}
|
||||
triggerClassName="!w-5 !h-5"
|
||||
/>
|
||||
{allowNodeMenu && (
|
||||
<PanelOperator
|
||||
id={id}
|
||||
data={data}
|
||||
offset={0}
|
||||
onOpenChange={handleOpenChange}
|
||||
triggerClassName="!w-5 !h-5"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
Handle,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import BlockSelector from '../../../block-selector'
|
||||
import {
|
||||
@ -46,6 +47,8 @@ export const NodeTargetHandle = memo(({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const interactionMode = useHooksStore(s => s.interactionMode)
|
||||
const allowGraphActions = interactionMode !== 'subgraph'
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availablePrevBlocks.length
|
||||
@ -55,9 +58,9 @@ export const NodeTargetHandle = memo(({
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!connected)
|
||||
if (!connected && allowGraphActions)
|
||||
setOpen(v => !v)
|
||||
}, [connected])
|
||||
}, [allowGraphActions, connected])
|
||||
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
@ -91,11 +94,11 @@ export const NodeTargetHandle = memo(({
|
||||
|| data.type === BlockEnum.TriggerPlugin) && 'opacity-0',
|
||||
handleClassName,
|
||||
)}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
isConnectable={allowGraphActions && isConnectable}
|
||||
onClick={allowGraphActions ? handleHandleClick : undefined}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !getNodesReadOnly() && (
|
||||
allowGraphActions && !connected && isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
@ -135,6 +138,8 @@ export const NodeSourceHandle = memo(({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const interactionMode = useHooksStore(s => s.interactionMode)
|
||||
const allowGraphActions = interactionMode !== 'subgraph'
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availableNextBlocks.length
|
||||
const isChatMode = useIsChatMode()
|
||||
@ -145,8 +150,9 @@ export const NodeSourceHandle = memo(({
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setOpen(v => !v)
|
||||
}, [])
|
||||
if (allowGraphActions)
|
||||
setOpen(v => !v)
|
||||
}, [allowGraphActions])
|
||||
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
@ -161,7 +167,7 @@ export const NodeSourceHandle = memo(({
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoOpenStartNodeSelector)
|
||||
if (!shouldAutoOpenStartNodeSelector || !allowGraphActions)
|
||||
return
|
||||
|
||||
if (isChatMode) {
|
||||
@ -198,8 +204,8 @@ export const NodeSourceHandle = memo(({
|
||||
!connected && 'after:opacity-0',
|
||||
handleClassName,
|
||||
)}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
isConnectable={allowGraphActions && isConnectable}
|
||||
onClick={allowGraphActions ? handleHandleClick : undefined}
|
||||
>
|
||||
<div className="absolute -top-1 left-1/2 hidden -translate-x-1/2 -translate-y-full rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 shadow-lg group-hover/handle:block">
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
@ -214,7 +220,7 @@ export const NodeSourceHandle = memo(({
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isConnectable && !getNodesReadOnly() && (
|
||||
allowGraphActions && isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
|
||||
@ -231,6 +231,8 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
} = useNodesMetaData()
|
||||
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const interactionMode = useHooksStore(s => s.interactionMode)
|
||||
const allowGraphActions = interactionMode !== 'subgraph'
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
@ -514,9 +516,9 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<HelpLink nodeType={data.type} />
|
||||
<PanelOperator id={id} data={data} showHelpLink={false} />
|
||||
<div className="mx-3 h-3.5 w-[1px] bg-divider-regular" />
|
||||
{allowGraphActions && <HelpLink nodeType={data.type} />}
|
||||
{allowGraphActions && <PanelOperator id={id} data={data} showHelpLink={false} />}
|
||||
{allowGraphActions && <div className="mx-3 h-3.5 w-[1px] bg-divider-regular" />}
|
||||
<div
|
||||
className="flex h-6 w-6 cursor-pointer items-center justify-center"
|
||||
onClick={() => handleNodeSelect(id, true)}
|
||||
@ -623,7 +625,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
allowGraphActions && hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
@ -631,7 +633,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
allowGraphActions && hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
@ -639,7 +641,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
allowGraphActions && !!availableNextBlocks.length && (
|
||||
<div className="border-t-[0.5px] border-divider-regular p-4">
|
||||
<div className="system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary">
|
||||
{t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
@ -651,7 +653,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{readmeEntranceComponent}
|
||||
{allowGraphActions ? readmeEntranceComponent : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { SubGraphModalProps } from './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 } from '@/app/components/workflow/types'
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { Fragment, memo } from 'react'
|
||||
import { Fragment, memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { PromptRole } from '@/app/components/workflow/types'
|
||||
import SubGraphCanvas from './sub-graph-canvas'
|
||||
|
||||
const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
@ -19,6 +26,76 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
agentNodeId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const reactflowStore = useStoreApi()
|
||||
const workflowNodes = useStore(state => state.nodes)
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
|
||||
const extractorNode = useMemo(() => {
|
||||
return workflowNodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType> | undefined
|
||||
}, [extractorNodeId, workflowNodes])
|
||||
const toolNode = useMemo(() => {
|
||||
return workflowNodes.find(node => node.id === toolNodeId)
|
||||
}, [toolNodeId, workflowNodes])
|
||||
const toolParamValue = (toolNode?.data as ToolNodeType | undefined)?.tool_parameters?.[paramKey]?.value as string | undefined
|
||||
|
||||
const getSystemPromptText = useCallback((promptTemplate?: PromptItem[] | PromptItem) => {
|
||||
if (!promptTemplate)
|
||||
return ''
|
||||
if (Array.isArray(promptTemplate)) {
|
||||
const systemPrompt = promptTemplate.find(item => item.role === PromptRole.system)
|
||||
return systemPrompt?.text || ''
|
||||
}
|
||||
return promptTemplate.text || ''
|
||||
}, [])
|
||||
|
||||
const handleSave = useCallback((subGraphNodes: any[], _edges: any[]) => {
|
||||
const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId)
|
||||
if (!extractorNodeData)
|
||||
return
|
||||
|
||||
const systemPromptText = getSystemPromptText(extractorNodeData.data?.prompt_template)
|
||||
const placeholder = `{{@${agentNodeId}.context@}}`
|
||||
const nextValue = `${placeholder}${systemPromptText}`
|
||||
|
||||
const { getNodes, setNodes } = reactflowStore.getState()
|
||||
const nextNodes = getNodes().map((node) => {
|
||||
if (node.id === extractorNodeId) {
|
||||
return {
|
||||
...node,
|
||||
hidden: true,
|
||||
data: {
|
||||
...node.data,
|
||||
...extractorNodeData.data,
|
||||
parent_node_id: toolNodeId,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (node.id === toolNodeId) {
|
||||
const toolData = node.data as ToolNodeType
|
||||
if (!toolData.tool_parameters?.[paramKey])
|
||||
return node
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...toolData,
|
||||
tool_parameters: {
|
||||
...toolData.tool_parameters,
|
||||
[paramKey]: {
|
||||
...toolData.tool_parameters[paramKey],
|
||||
value: nextValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
setNodes(nextNodes)
|
||||
// Trigger main graph draft sync to persist changes to backend
|
||||
handleSyncWorkflowDraft()
|
||||
}, [agentNodeId, extractorNodeId, getSystemPromptText, handleSyncWorkflowDraft, paramKey, reactflowStore, toolNodeId])
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
@ -58,6 +135,9 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
sourceVariable={sourceVariable}
|
||||
agentNodeId={agentNodeId}
|
||||
agentName={agentName}
|
||||
extractorNode={extractorNode}
|
||||
toolParamValue={toolParamValue}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
|
||||
@ -10,6 +10,9 @@ const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
|
||||
sourceVariable,
|
||||
agentNodeId,
|
||||
agentName,
|
||||
extractorNode,
|
||||
toolParamValue,
|
||||
onSave,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
@ -19,6 +22,9 @@ const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
|
||||
sourceVariable={sourceVariable}
|
||||
agentNodeId={agentNodeId}
|
||||
agentName={agentName}
|
||||
extractorNode={extractorNode}
|
||||
toolParamValue={toolParamValue}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types'
|
||||
|
||||
type WorkflowValueSelector = string[]
|
||||
|
||||
export type SubGraphModalProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: ValueSelector
|
||||
sourceVariable: WorkflowValueSelector
|
||||
agentName: string
|
||||
agentNodeId: string
|
||||
}
|
||||
@ -13,7 +16,10 @@ export type SubGraphModalProps = {
|
||||
export type SubGraphCanvasProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: ValueSelector
|
||||
sourceVariable: WorkflowValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
extractorNode?: WorkflowNode<LLMNodeType>
|
||||
toolParamValue?: string
|
||||
onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user