From ee9ac15174cb90433c5ce590d629d465d513a3ec Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 3 Nov 2025 19:19:26 +0800 Subject: [PATCH] Feat: Fixed an issue where dragged operators within an iteration were not associated with the iteration. #10866 (#10969) ### What problem does this PR solve? Feat: Fixed an issue where dragged operators within an iteration were not associated with the iteration. #10866 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/locales/en.ts | 1 + web/src/locales/zh.ts | 1 + web/src/pages/agent/canvas/index.tsx | 15 +- web/src/pages/agent/constant/index.tsx | 1 + .../structured-output-secondary-menu.tsx | 6 +- .../agent/form/user-fill-up-form/index.tsx | 8 +- web/src/pages/agent/hooks.tsx | 217 +----------------- .../pages/agent/hooks/use-connection-drag.ts | 24 +- .../agent/hooks/use-dropdown-position.ts | 5 +- .../agent/hooks/use-placeholder-manager.ts | 3 +- 10 files changed, 40 insertions(+), 241 deletions(-) diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index df4a32227..3c2aeec4d 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1724,6 +1724,7 @@ Important structured information may include: names, dates, locations, events, k }, structuredOutput: { configuration: 'Configuration', + structuredOutput: 'Structured output', }, }, llmTools: { diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 434973870..bd9a66a4b 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1605,6 +1605,7 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?', structuredOutput: { configuration: '配置', + structuredOutput: '结构化输出', }, }, footer: { diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 8cff4b7ae..4806eeb53 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -13,6 +13,7 @@ import { NodeTypes, Position, ReactFlow, + ReactFlowInstance, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { NotebookPen } from 'lucide-react'; @@ -27,11 +28,7 @@ import { } from '../context'; import FormSheet from '../form-sheet/next'; -import { - useHandleDrop, - useSelectCanvasData, - useValidateConnection, -} from '../hooks'; +import { useSelectCanvasData, useValidateConnection } from '../hooks'; import { useAddNode } from '../hooks/use-add-node'; import { useBeforeDelete } from '../hooks/use-before-delete'; import { useCacheChatLog } from '../hooks/use-cache-chat-log'; @@ -124,8 +121,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { } = useSelectCanvasData(); const isValidConnection = useValidateConnection(); - const { onDrop, onDragOver, setReactFlowInstance, reactFlowInstance } = - useHandleDrop(); + const [reactFlowInstance, setReactFlowInstance] = + useState>(); const { onNodeClick, @@ -204,7 +201,6 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { onMove, nodeId, } = useConnectionDrag( - reactFlowInstance, originalOnConnect, showModal, hideModal, @@ -214,6 +210,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { removePlaceholderNode, clearActiveDropdown, checkAndRemoveExistingPlaceholder, + reactFlowInstance, ); const onPaneClick = useCallback(() => { @@ -286,11 +283,9 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { onConnect={handleConnect} nodeTypes={nodeTypes} edgeTypes={edgeTypes} - onDrop={onDrop} onConnectStart={onConnectStart} onConnectEnd={onConnectEnd} onMove={onMove} - onDragOver={onDragOver} onNodeClick={onNodeClick} onPaneClick={onPaneClick} onInit={setReactFlowInstance} diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index f8a0c5d79..0cff21c9d 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -803,6 +803,7 @@ export const RestrictedUpstreamMap = { [Operator.HierarchicalMerger]: [Operator.Begin], [Operator.Tokenizer]: [Operator.Begin], [Operator.Extractor]: [Operator.Begin], + [Operator.File]: [Operator.Begin], }; export const NodeMap = { diff --git a/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx b/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx index 4c18fd7b1..ee18fc8aa 100644 --- a/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx +++ b/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx @@ -7,6 +7,7 @@ import { cn } from '@/lib/utils'; import { get, isEmpty, isPlainObject } from 'lodash'; import { ChevronRight } from 'lucide-react'; import { PropsWithChildren, ReactNode, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { JsonSchemaDataType, VariableType } from '../../constant'; import { useGetStructuredOutputByValue } from '../../hooks/use-build-structured-output'; import { @@ -27,6 +28,7 @@ export function StructuredOutputSecondaryMenu({ click, type, }: StructuredOutputSecondaryMenuProps) { + const { t } = useTranslation(); const filterStructuredOutput = useGetStructuredOutputByValue(); const structuredOutput = filterStructuredOutput(data.value); @@ -113,7 +115,9 @@ export function StructuredOutputSecondaryMenu({ )} >
-
{data?.parentLabel} structured output:
+
+ {t('flow.structuredOutput.structuredOutput')} +
{renderAgentStructuredOutput(structuredOutput, data)}
diff --git a/web/src/pages/agent/form/user-fill-up-form/index.tsx b/web/src/pages/agent/form/user-fill-up-form/index.tsx index 68def5601..96087de7a 100644 --- a/web/src/pages/agent/form/user-fill-up-form/index.tsx +++ b/web/src/pages/agent/form/user-fill-up-form/index.tsx @@ -9,7 +9,6 @@ import { FormMessage, } from '@/components/ui/form'; import { Switch } from '@/components/ui/switch'; -import { Textarea } from '@/components/ui/textarea'; import { FormTooltip } from '@/components/ui/tooltip'; import { zodResolver } from '@hookform/resolvers/zod'; import { Plus } from 'lucide-react'; @@ -22,6 +21,7 @@ import { ParameterDialog } from '../begin-form/parameter-dialog'; import { QueryTable } from '../begin-form/query-table'; import { useEditQueryRecord } from '../begin-form/use-edit-query'; import { Output } from '../components/output'; +import { PromptEditor } from '../components/prompt-editor'; import { useValues } from './use-values'; import { useWatchFormChange } from './use-watch-change'; @@ -108,11 +108,7 @@ function UserFillUpForm({ node }: INextOperatorForm) { {t('flow.msg')} - + diff --git a/web/src/pages/agent/hooks.tsx b/web/src/pages/agent/hooks.tsx index 77a53abb8..59bbe3bc9 100644 --- a/web/src/pages/agent/hooks.tsx +++ b/web/src/pages/agent/hooks.tsx @@ -1,72 +1,14 @@ -import { - Connection, - Edge, - getOutgoers, - Node, - Position, - ReactFlowInstance, -} from '@xyflow/react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Connection, Edge, getOutgoers } from '@xyflow/react'; +import React, { useCallback, useEffect } from 'react'; // import { shallow } from 'zustand/shallow'; import { settledModelVariableMap } from '@/constants/knowledge'; -import { useFetchModelId } from '@/hooks/logic-hooks'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { humanId } from 'human-id'; import { get, lowerFirst, omit } from 'lodash'; import { UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { - initialAgentValues, - initialAkShareValues, - initialArXivValues, - initialBaiduFanyiValues, - initialBaiduValues, - initialBeginValues, - initialBingValues, - initialCategorizeValues, - initialCodeValues, - initialCrawlerValues, - initialDeepLValues, - initialDuckValues, - initialEmailValues, - initialExeSqlValues, - initialGithubValues, - initialGoogleScholarValues, - initialGoogleValues, - initialInvokeValues, - initialIterationValues, - initialJin10Values, - initialKeywordExtractValues, - initialMessageValues, - initialNoteValues, - initialPubMedValues, - initialQWeatherValues, - initialRelevantValues, - initialRetrievalValues, - initialRewriteQuestionValues, - initialSearXNGValues, - initialStringTransformValues, - initialSwitchValues, - initialTavilyExtractValues, - initialTavilyValues, - initialTuShareValues, - initialUserFillUpValues, - initialWaitingDialogueValues, - initialWenCaiValues, - initialWikipediaValues, - initialYahooFinanceValues, - NodeMap, - Operator, - RestrictedUpstreamMap, -} from './constant'; +import { Operator, RestrictedUpstreamMap } from './constant'; import useGraphStore, { RFState } from './store'; -import { - buildCategorizeObjectFromList, - generateNodeNamesWithIncreasingIndex, - getNodeDragHandle, - getRelativePositionToIterationNode, - replaceIdWithText, -} from './utils'; +import { buildCategorizeObjectFromList, replaceIdWithText } from './utils'; const selector = (state: RFState) => ({ nodes: state.nodes, @@ -86,72 +28,6 @@ export const useSelectCanvasData = () => { return useGraphStore(selector); }; -export const useInitializeOperatorParams = () => { - const llmId = useFetchModelId(); - - const initialFormValuesMap = useMemo(() => { - return { - [Operator.Begin]: initialBeginValues, - [Operator.Retrieval]: initialRetrievalValues, - [Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId }, - [Operator.Relevant]: { ...initialRelevantValues, llm_id: llmId }, - [Operator.RewriteQuestion]: { - ...initialRewriteQuestionValues, - llm_id: llmId, - }, - [Operator.Message]: initialMessageValues, - [Operator.KeywordExtract]: { - ...initialKeywordExtractValues, - llm_id: llmId, - }, - [Operator.DuckDuckGo]: initialDuckValues, - [Operator.Baidu]: initialBaiduValues, - [Operator.Wikipedia]: initialWikipediaValues, - [Operator.PubMed]: initialPubMedValues, - [Operator.ArXiv]: initialArXivValues, - [Operator.Google]: initialGoogleValues, - [Operator.Bing]: initialBingValues, - [Operator.GoogleScholar]: initialGoogleScholarValues, - [Operator.DeepL]: initialDeepLValues, - [Operator.SearXNG]: initialSearXNGValues, - [Operator.GitHub]: initialGithubValues, - [Operator.BaiduFanyi]: initialBaiduFanyiValues, - [Operator.QWeather]: initialQWeatherValues, - [Operator.ExeSQL]: { ...initialExeSqlValues, llm_id: llmId }, - [Operator.Switch]: initialSwitchValues, - [Operator.WenCai]: initialWenCaiValues, - [Operator.AkShare]: initialAkShareValues, - [Operator.YahooFinance]: initialYahooFinanceValues, - [Operator.Jin10]: initialJin10Values, - [Operator.TuShare]: initialTuShareValues, - [Operator.Note]: initialNoteValues, - [Operator.Crawler]: initialCrawlerValues, - [Operator.Invoke]: initialInvokeValues, - [Operator.Email]: initialEmailValues, - [Operator.Iteration]: initialIterationValues, - [Operator.IterationStart]: initialIterationValues, - [Operator.Code]: initialCodeValues, - [Operator.WaitingDialogue]: initialWaitingDialogueValues, - [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, - [Operator.TavilySearch]: initialTavilyValues, - [Operator.TavilyExtract]: initialTavilyExtractValues, - [Operator.Tool]: {}, - [Operator.UserFillUp]: initialUserFillUpValues, - [Operator.StringTransform]: initialStringTransformValues, - [Operator.Placeholder]: {}, - }; - }, [llmId]); - - const initializeOperatorParams = useCallback( - (operatorName: Operator) => { - return initialFormValuesMap[operatorName]; - }, - [initialFormValuesMap], - ); - - return initializeOperatorParams; -}; - export const useHandleDrag = () => { const handleDragStart = useCallback( (operatorId: string) => (ev: React.DragEvent) => { @@ -173,91 +49,6 @@ export const useGetNodeName = () => { }; }; -export const useHandleDrop = () => { - const addNode = useGraphStore((state) => state.addNode); - const nodes = useGraphStore((state) => state.nodes); - const [reactFlowInstance, setReactFlowInstance] = - useState>(); - const initializeOperatorParams = useInitializeOperatorParams(); - const getNodeName = useGetNodeName(); - - const onDragOver = useCallback((event: React.DragEvent) => { - event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; - }, []); - - const onDrop = useCallback( - (event: React.DragEvent) => { - event.preventDefault(); - - const type = event.dataTransfer.getData('application/@xyflow/react'); - - // check if the dropped element is valid - if (typeof type === 'undefined' || !type) { - return; - } - - // 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 - const position = reactFlowInstance?.screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - const newNode: Node = { - id: `${type}:${humanId()}`, - type: NodeMap[type as Operator] || 'ragNode', - position: position || { - x: 0, - y: 0, - }, - data: { - label: `${type}`, - name: generateNodeNamesWithIncreasingIndex(getNodeName(type), nodes), - form: initializeOperatorParams(type as Operator), - }, - sourcePosition: Position.Right, - targetPosition: Position.Left, - dragHandle: getNodeDragHandle(type), - }; - - if (type === Operator.Iteration) { - newNode.width = 500; - newNode.height = 250; - const iterationStartNode: Node = { - id: `${Operator.IterationStart}:${humanId()}`, - type: 'iterationStartNode', - position: { x: 50, y: 100 }, - // draggable: false, - data: { - label: Operator.IterationStart, - name: Operator.IterationStart, - form: {}, - }, - parentId: newNode.id, - extent: 'parent', - }; - addNode(newNode); - addNode(iterationStartNode); - } else { - const subNodeOfIteration = getRelativePositionToIterationNode( - nodes, - position, - ); - if (subNodeOfIteration) { - newNode.parentId = subNodeOfIteration.parentId; - newNode.position = subNodeOfIteration.position; - newNode.extent = 'parent'; - } - addNode(newNode); - } - }, - [reactFlowInstance, getNodeName, nodes, initializeOperatorParams, addNode], - ); - - return { onDrop, onDragOver, setReactFlowInstance, reactFlowInstance }; -}; - export const useHandleFormValuesChange = ( operatorName: Operator, id?: string, diff --git a/web/src/pages/agent/hooks/use-connection-drag.ts b/web/src/pages/agent/hooks/use-connection-drag.ts index 7977d3d5a..02ca46faa 100644 --- a/web/src/pages/agent/hooks/use-connection-drag.ts +++ b/web/src/pages/agent/hooks/use-connection-drag.ts @@ -1,4 +1,10 @@ -import { Connection, OnConnectEnd, Position } from '@xyflow/react'; +import { + Connection, + OnConnectEnd, + OnConnectStart, + Position, + ReactFlowInstance, +} from '@xyflow/react'; import { useCallback, useRef } from 'react'; import { useDropdownManager } from '../canvas/context'; import { Operator, PREVENT_CLOSE_DELAY } from '../constant'; @@ -14,7 +20,6 @@ interface ConnectionStartParams { * Responsible for handling connection drag start and end logic */ export const useConnectionDrag = ( - reactFlowInstance: any, onConnect: (connection: Connection) => void, showModal: () => void, hideModal: () => void, @@ -27,6 +32,7 @@ export const useConnectionDrag = ( removePlaceholderNode: () => void, clearActiveDropdown: () => void, checkAndRemoveExistingPlaceholder: () => void, + reactFlowInstance?: ReactFlowInstance, ) => { // Reference for whether connection is established const isConnectedRef = useRef(false); @@ -43,7 +49,7 @@ export const useConnectionDrag = ( /** * Connection start handler function */ - const onConnectStart = useCallback((event: any, params: any) => { + const onConnectStart: OnConnectStart = useCallback((event, params) => { isConnectedRef.current = false; // Record mouse start position to detect click vs drag @@ -141,16 +147,16 @@ export const useConnectionDrag = ( }, [ setDropdownPosition, - addCanvasNode, - setCreatedPlaceholderRef, - reactFlowInstance, - calculateDropdownPosition, - setActiveDropdown, - showModal, checkAndRemoveExistingPlaceholder, + addCanvasNode, + reactFlowInstance, removePlaceholderNode, hideModal, clearActiveDropdown, + setCreatedPlaceholderRef, + calculateDropdownPosition, + setActiveDropdown, + showModal, ], ); diff --git a/web/src/pages/agent/hooks/use-dropdown-position.ts b/web/src/pages/agent/hooks/use-dropdown-position.ts index fc3094c7c..58ef0cebc 100644 --- a/web/src/pages/agent/hooks/use-dropdown-position.ts +++ b/web/src/pages/agent/hooks/use-dropdown-position.ts @@ -1,3 +1,4 @@ +import { ReactFlowInstance } from '@xyflow/react'; import { useCallback } from 'react'; import { DROPDOWN_HORIZONTAL_OFFSET, @@ -9,7 +10,9 @@ import { * Dropdown position calculation Hook * Responsible for calculating dropdown menu position relative to placeholder node */ -export const useDropdownPosition = (reactFlowInstance: any) => { +export const useDropdownPosition = ( + reactFlowInstance?: ReactFlowInstance, +) => { /** * Calculate dropdown menu position * @param clientX Mouse click screen X coordinate diff --git a/web/src/pages/agent/hooks/use-placeholder-manager.ts b/web/src/pages/agent/hooks/use-placeholder-manager.ts index e970908db..245a42f75 100644 --- a/web/src/pages/agent/hooks/use-placeholder-manager.ts +++ b/web/src/pages/agent/hooks/use-placeholder-manager.ts @@ -1,3 +1,4 @@ +import { pick } from 'lodash'; import { useCallback, useRef } from 'react'; import { Operator } from '../constant'; import useGraphStore from '../store'; @@ -113,7 +114,7 @@ export const usePlaceholderManager = (reactFlowInstance: any) => { if (newNode) { updateNode({ ...newNode, - position: placeholderNode.position, + ...pick(placeholderNode, ['position', 'parentId', 'extent']), }); } }