From 1c68c9ebd6cbb68da263b936f2a839501a4d12ff Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 24 Jun 2025 18:01:30 +0800 Subject: [PATCH] Feat: Add IterationNode component #3221 (#8461) ### What problem does this PR solve? Feat: Add IterationNode component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/pages/agent/canvas/index.less | 1 + .../agent/canvas/node/iteration-node.tsx | 69 ++++++-------- web/src/pages/agent/constant.tsx | 3 +- .../agent/form-sheet/use-form-config-map.tsx | 2 +- web/src/pages/agent/form/agent-form/index.tsx | 66 +++++++++---- .../agent/form/components/query-variable.tsx | 6 +- .../pages/agent/form/iteration-form/index.tsx | 61 ++++++++++++ .../agent/form/iteration-form/use-values.ts | 25 +++++ .../pages/agent/form/iteration-from/index.tsx | 94 ------------------- web/src/pages/agent/hooks/use-add-node.ts | 33 ++++--- 10 files changed, 191 insertions(+), 169 deletions(-) create mode 100644 web/src/pages/agent/form/iteration-form/index.tsx create mode 100644 web/src/pages/agent/form/iteration-form/use-values.ts delete mode 100644 web/src/pages/agent/form/iteration-from/index.tsx diff --git a/web/src/pages/agent/canvas/index.less b/web/src/pages/agent/canvas/index.less index d824d88f1..21f72e150 100644 --- a/web/src/pages/agent/canvas/index.less +++ b/web/src/pages/agent/canvas/index.less @@ -3,6 +3,7 @@ height: 100%; :global(.react-flow__node-group) { .commonNode(); + border-radius: 0 0 10px 10px; padding: 0; border: 0; background-color: transparent; diff --git a/web/src/pages/agent/canvas/node/iteration-node.tsx b/web/src/pages/agent/canvas/node/iteration-node.tsx index 53a84835d..a6094f97b 100644 --- a/web/src/pages/agent/canvas/node/iteration-node.tsx +++ b/web/src/pages/agent/canvas/node/iteration-node.tsx @@ -1,15 +1,17 @@ -import { useTheme } from '@/components/theme-provider'; import { IIterationNode, IIterationStartNode, } from '@/interfaces/database/flow'; import { cn } from '@/lib/utils'; -import { Handle, NodeProps, NodeResizeControl, Position } from '@xyflow/react'; -import { ListRestart } from 'lucide-react'; +import { NodeProps, NodeResizeControl, Position } from '@xyflow/react'; import { memo } from 'react'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import { NodeHandleId, Operator } from '../../constant'; +import OperatorIcon from '../../operator-icon'; +import { CommonHandle } from './handle'; +import { RightHandleStyle } from './handle-icon'; import styles from './index.less'; import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; function ResizeIcon() { return ( @@ -50,47 +52,43 @@ export function InnerIterationNode({ isConnectable = true, selected, }: NodeProps) { - const { theme } = useTheme(); + // const { theme } = useTheme(); return (
- - + + nodeId={id} + > + ) { - const { theme } = useTheme(); - return ( -
- + + id={NodeHandleId.Start} + nodeId={id} + >
- +
-
+ ); } diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index aa5ef47da..59dc764ed 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -644,7 +644,7 @@ export const initialEmailValues = { }; export const initialIterationValues = { - delimiter: ',', + items_ref: '', }; export const initialIterationStartValues = {}; @@ -665,6 +665,7 @@ export const initialWaitingDialogueValues = {}; export const initialAgentValues = { ...initialLlmBaseValues, + description: '', sys_prompt: ``, prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }], message_history_window_size: 12, diff --git a/web/src/pages/agent/form-sheet/use-form-config-map.tsx b/web/src/pages/agent/form-sheet/use-form-config-map.tsx index 267682a1a..44a112657 100644 --- a/web/src/pages/agent/form-sheet/use-form-config-map.tsx +++ b/web/src/pages/agent/form-sheet/use-form-config-map.tsx @@ -23,7 +23,7 @@ import GithubForm from '../form/github-form'; import GoogleForm from '../form/google-form'; import GoogleScholarForm from '../form/google-scholar-form'; import InvokeForm from '../form/invoke-form'; -import IterationForm from '../form/iteration-from'; +import IterationForm from '../form/iteration-form'; import Jin10Form from '../form/jin10-form'; import KeywordExtractForm from '../form/keyword-extract-form'; import MessageForm from '../form/message-form'; diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 5a348d1e5..fe7b8af86 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -10,15 +10,17 @@ import { FormItem, FormLabel, } from '@/components/ui/form'; +import { Textarea } from '@/components/ui/textarea'; import { zodResolver } from '@hookform/resolvers/zod'; import { Position } from '@xyflow/react'; import { useContext, useMemo } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { Operator, initialAgentValues } from '../../constant'; +import { NodeHandleId, Operator, initialAgentValues } from '../../constant'; import { AgentInstanceContext } from '../../context'; import { INextOperatorForm } from '../../interface'; +import useGraphStore from '../../store'; import { Output } from '../components/output'; import { PromptEditor } from '../components/prompt-editor'; import { AgentTools } from './agent-tools'; @@ -27,6 +29,7 @@ import { useWatchFormChange } from './use-watch-change'; const FormSchema = z.object({ sys_prompt: z.string(), + description: z.string().optional(), prompts: z.string().optional(), // prompts: z // .array( @@ -49,9 +52,17 @@ const FormSchema = z.object({ const AgentForm = ({ node }: INextOperatorForm) => { const { t } = useTranslation(); + const { edges } = useGraphStore((state) => state); const defaultValues = useValues(node); + const isSubAgent = useMemo(() => { + const edge = edges.find( + (x) => x.target === node?.id && x.targetHandle === NodeHandleId.AgentTop, + ); + return !!edge; + }, [edges, node?.id]); + const outputList = useMemo(() => { return [ { title: 'content', type: initialAgentValues.outputs.content.type }, @@ -76,6 +87,20 @@ const AgentForm = ({ node }: INextOperatorForm) => { }} > + {isSubAgent && ( + ( + + Description + + + + + )} + /> + )} { /> - - {/* */} - ( - - User Prompt - -
- -
-
-
- )} - /> -
+ {isSubAgent || ( + + {/* */} + ( + + User Prompt + +
+ +
+
+
+ )} + /> +
+ )} ( {t('flow.query')} diff --git a/web/src/pages/agent/form/iteration-form/index.tsx b/web/src/pages/agent/form/iteration-form/index.tsx new file mode 100644 index 000000000..bb9b7436f --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/index.tsx @@ -0,0 +1,61 @@ +import { FormContainer } from '@/components/form-container'; +import { Form } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { initialRetrievalValues } from '../../constant'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; +import { useValues } from './use-values'; + +const FormSchema = z.object({ + query: z.string().optional(), + similarity_threshold: z.coerce.number(), + keywords_similarity_weight: z.coerce.number(), + top_n: z.coerce.number(), + top_k: z.coerce.number(), + kb_ids: z.array(z.string()), + rerank_id: z.string(), + empty_response: z.string(), +}); + +const IterationForm = ({ node }: INextOperatorForm) => { + const outputList = useMemo(() => { + return [ + { + title: 'formalized_content', + type: initialRetrievalValues.outputs.formalized_content.type, + }, + ]; + }, []); + + const defaultValues = useValues(node); + + const form = useForm({ + defaultValues: defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
+ { + e.preventDefault(); + }} + > + + + + +
+ + ); +}; + +export default IterationForm; diff --git a/web/src/pages/agent/form/iteration-form/use-values.ts b/web/src/pages/agent/form/iteration-form/use-values.ts new file mode 100644 index 000000000..2f8479efe --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/use-values.ts @@ -0,0 +1,25 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { initialIterationValues } from '../../constant'; + +export function useValues(node?: RAGFlowNodeType) { + const defaultValues = useMemo( + () => ({ + ...initialIterationValues, + }), + [], + ); + + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + return formData; + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/agent/form/iteration-from/index.tsx b/web/src/pages/agent/form/iteration-from/index.tsx deleted file mode 100644 index f0f23918a..000000000 --- a/web/src/pages/agent/form/iteration-from/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { CommaIcon, SemicolonIcon } from '@/assets/icon/Icon'; -import { Form, Select } from 'antd'; -import { - CornerDownLeft, - IndentIncrease, - Minus, - Slash, - Underline, -} from 'lucide-react'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const optionList = [ - { - value: ',', - icon: CommaIcon, - text: 'comma', - }, - { - value: '\n', - icon: CornerDownLeft, - text: 'lineBreak', - }, - { - value: 'tab', - icon: IndentIncrease, - text: 'tab', - }, - { - value: '_', - icon: Underline, - text: 'underline', - }, - { - value: '/', - icon: Slash, - text: 'diagonal', - }, - { - value: '-', - icon: Minus, - text: 'minus', - }, - { - value: ';', - icon: SemicolonIcon, - text: 'semicolon', - }, -]; - -const IterationForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslation(); - - const options = useMemo(() => { - return optionList.map((x) => { - let Icon = x.icon; - - return { - value: x.value, - label: ( -
- - {t(`flow.delimiterOptions.${x.text}`)} -
- ), - }; - }); - }, [t]); - - return ( -
- - - - -
- ); -}; - -export default IterationForm; diff --git a/web/src/pages/agent/hooks/use-add-node.ts b/web/src/pages/agent/hooks/use-add-node.ts index 2ae543dd0..d4008ee01 100644 --- a/web/src/pages/agent/hooks/use-add-node.ts +++ b/web/src/pages/agent/hooks/use-add-node.ts @@ -51,7 +51,6 @@ import useGraphStore from '../store'; import { generateNodeNamesWithIncreasingIndex, getNodeDragHandle, - getRelativePositionToIterationNode, } from '../utils'; export const useInitializeOperatorParams = () => { @@ -234,11 +233,9 @@ function useAddToolNode() { } export function useAddNode(reactFlowInstance?: ReactFlowInstance) { - const addNode = useGraphStore((state) => state.addNode); - const getNode = useGraphStore((state) => state.getNode); - const addEdge = useGraphStore((state) => state.addEdge); - const nodes = useGraphStore((state) => state.nodes); - const edges = useGraphStore((state) => state.edges); + const { edges, nodes, addEdge, addNode, getNode } = useGraphStore( + (state) => state, + ); const getNodeName = useGetNodeName(); const initializeOperatorParams = useInitializeOperatorParams(); const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); @@ -257,6 +254,8 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { (event?: React.MouseEvent) => { const nodeId = params.nodeId; + const node = getNode(nodeId); + // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition // and you don't need to subtract the reactFlowBounds.left/top anymore // details: https://@xyflow/react.dev/whats-new/2023-11-10 @@ -289,6 +288,11 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { dragHandle: getNodeDragHandle(type), }; + if (node && node.parentId) { + newNode.parentId = node.parentId; + newNode.extent = 'parent'; + } + if (type === Operator.Iteration) { newNode.width = 500; newNode.height = 250; @@ -307,6 +311,14 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { }; addNode(newNode); addNode(iterationStartNode); + if (nodeId) { + addEdge({ + source: nodeId, + target: newNode.id, + sourceHandle: NodeHandleId.Start, + targetHandle: NodeHandleId.End, + }); + } } else if ( type === Operator.Agent && params.position === Position.Bottom @@ -345,15 +357,6 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { } else if (type === Operator.Tool) { addToolNode(newNode, params.nodeId); } else { - const subNodeOfIteration = getRelativePositionToIterationNode( - nodes, - position, - ); - if (subNodeOfIteration) { - newNode.parentId = subNodeOfIteration.parentId; - newNode.position = subNodeOfIteration.position; - newNode.extent = 'parent'; - } addNode(newNode); addChildEdge(params.position, { source: params.nodeId,