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)
This commit is contained in:
balibabu
2025-11-03 19:19:26 +08:00
committed by GitHub
parent ac465ba2a6
commit ee9ac15174
10 changed files with 40 additions and 241 deletions

View File

@ -1724,6 +1724,7 @@ Important structured information may include: names, dates, locations, events, k
},
structuredOutput: {
configuration: 'Configuration',
structuredOutput: 'Structured output',
},
},
llmTools: {

View File

@ -1605,6 +1605,7 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?',
structuredOutput: {
configuration: '配置',
structuredOutput: '结构化输出',
},
},
footer: {

View File

@ -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<ReactFlowInstance<any, any>>();
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}

View File

@ -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 = {

View File

@ -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({
)}
>
<section className="p-2">
<div className="p-1">{data?.parentLabel} structured output:</div>
<div className="p-1">
{t('flow.structuredOutput.structuredOutput')}
</div>
{renderAgentStructuredOutput(structuredOutput, data)}
</section>
</HoverCardContent>

View File

@ -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')}
</FormLabel>
<FormControl>
<Textarea
rows={5}
{...field}
placeholder={t('common.pleaseInput')}
></Textarea>
<PromptEditor value={field.value} onChange={field.onChange} />
</FormControl>
<FormMessage />
</FormItem>

View File

@ -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<HTMLDivElement>) => {
@ -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<ReactFlowInstance<any, any>>();
const initializeOperatorParams = useInitializeOperatorParams();
const getNodeName = useGetNodeName();
const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
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<any> = {
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<any> = {
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,

View File

@ -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<any, any>,
) => {
// 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,
],
);

View File

@ -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<any, any>,
) => {
/**
* Calculate dropdown menu position
* @param clientX Mouse click screen X coordinate

View File

@ -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']),
});
}
}