diff --git a/web/app/components/sub-graph/components/sub-graph-children.tsx b/web/app/components/sub-graph/components/sub-graph-children.tsx index ec1f9ee4d6..c9cb989f53 100644 --- a/web/app/components/sub-graph/components/sub-graph-children.tsx +++ b/web/app/components/sub-graph/components/sub-graph-children.tsx @@ -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 = ({ 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, diff --git a/web/app/components/sub-graph/components/sub-graph-main.tsx b/web/app/components/sub-graph/components/sub-graph-main.tsx index e4d784fb96..15627b8642 100644 --- a/web/app/components/sub-graph/components/sub-graph-main.tsx +++ b/web/app/components/sub-graph/components/sub-graph-main.tsx @@ -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 = ({ 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 = ({ resetConversationVar: async () => {}, invalidateConversationVarValues: () => {}, } - }, [availableNodesMetaData, handleNodesChange, nodes]) + }, [availableNodesMetaData]) return ( = ({ edges={edges} viewport={viewport} hooksStore={hooksStore as any} + allowSelectionWhenReadOnly + canvasReadOnly + interactionMode="subgraph" > = (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) diff --git a/web/app/components/sub-graph/store/index.ts b/web/app/components/sub-graph/store/index.ts index 798fc93e6a..17f6bde319 100644 --- a/web/app/components/sub-graph/store/index.ts +++ b/web/app/components/sub-graph/store/index.ts @@ -5,6 +5,7 @@ const initialState: Omit + 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[] diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index d88c37e35d..f727d1fa0d 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -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, }} /> - -
- 'hover:scale-150 transition-all'} - /> -
-
+ {allowGraphActions && ( + +
+ 'hover:scale-150 transition-all'} + /> +
+
+ )} ) } diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index 44014fc0d7..989c5e5063 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -23,6 +23,7 @@ export type AvailableNodesMetaData = { nodesMap?: Record> } 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) => { return createStore(set => ({ refreshAll: props => set(state => ({ ...state, ...props })), + interactionMode, doSyncWorkflowDraft, syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft, diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 0845dbd2d9..64e9e9f794 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -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]) } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index f834b06260..1daf06dd02 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -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 { diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 46967b9688..1a2a42bcde 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -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 = memo(({ nodes: originalNodes, @@ -141,6 +146,9 @@ export const Workflow: FC = memo(({ viewport, children, onWorkflowDataUpdate, + allowSelectionWhenReadOnly = false, + canvasReadOnly = false, + interactionMode = 'default', }) => { const workflowContainerRef = useRef(null) const workflowStore = useWorkflowStore() @@ -196,7 +204,7 @@ export const Workflow: FC = 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 = memo(({ }, }) - useShortcuts() + const isSubGraph = interactionMode === 'subgraph' + useShortcuts(!isSubGraph) // Initialize workflow node search functionality useWorkflowSearch() @@ -382,6 +391,16 @@ export const Workflow: FC = memo(({ } } + const handleNodeClickInMode = useCallback( + (event, node) => { + if (isSubGraph && node.data.type !== BlockEnum.LLM) + return + + handleNodeClick(event, node) + }, + [handleNodeClick, isSubGraph], + ) + return (
= memo(({ ref={workflowContainerRef} > - + {!isSubGraph && }
- + {!isSubGraph && }
- - - - - + {!isSubGraph && } + {!isSubGraph && } + {!isSubGraph && } + {!isSubGraph && } + {!isSubGraph && } { !!showConfirm && ( = 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} > = ({ 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 (
= ({ onClick={e => e.stopPropagation()} > { - canRunBySingle(data.type, isChildNode) && ( + canSingleRun && (
{ @@ -80,13 +87,15 @@ const NodeControl: FC = ({
) } - + {allowNodeMenu && ( + + )}
) diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index 1bd8ea84e8..30e23e4f6f 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -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() && ( 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} >
@@ -214,7 +220,7 @@ export const NodeSourceHandle = memo(({
{ - isConnectable && !getNodesReadOnly() && ( + allowGraphActions && isConnectable && !getNodesReadOnly() && ( = ({ } = 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 = ({ ) } - - -
+ {allowGraphActions && } + {allowGraphActions && } + {allowGraphActions &&
}
handleNodeSelect(id, true)} @@ -623,7 +625,7 @@ const BasePanel: FC = ({
{ - hasRetryNode(data.type) && ( + allowGraphActions && hasRetryNode(data.type) && ( = ({ ) } { - hasErrorHandleNode(data.type) && ( + allowGraphActions && hasErrorHandleNode(data.type) && ( = ({ ) } { - !!availableNextBlocks.length && ( + allowGraphActions && !!availableNextBlocks.length && (
{t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()} @@ -651,7 +653,7 @@ const BasePanel: FC = ({
) } - {readmeEntranceComponent} + {allowGraphActions ? readmeEntranceComponent : null}
)} diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx index ee550b9c86..68a985dccc 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx @@ -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 = ({ @@ -19,6 +26,76 @@ const SubGraphModal: FC = ({ 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 | 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 ( @@ -58,6 +135,9 @@ const SubGraphModal: FC = ({ sourceVariable={sourceVariable} agentNodeId={agentNodeId} agentName={agentName} + extractorNode={extractorNode} + toolParamValue={toolParamValue} + onSave={handleSave} />
diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx index f13a48d87c..120015e189 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx @@ -10,6 +10,9 @@ const SubGraphCanvas: FC = ({ sourceVariable, agentNodeId, agentName, + extractorNode, + toolParamValue, + onSave, }) => { return (
@@ -19,6 +22,9 @@ const SubGraphCanvas: FC = ({ sourceVariable={sourceVariable} agentNodeId={agentNodeId} agentName={agentName} + extractorNode={extractorNode} + toolParamValue={toolParamValue} + onSave={onSave} />
) diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts index 4b33b0cfde..4023b258c7 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts @@ -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 + toolParamValue?: string + onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void }