mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-08 13:55:06 -05:00
fix(frontend): improve history tracking and error handling in flow editor (#11784)
### Changes 🏗️ - Improved error handling in `useRunInputDialog.ts` to properly handle cases where node errors are empty or undefined - Fixed node collision resolution in Flow component by using the current state from the store instead of stale props - Enhanced edge management to track history when edges are removed - Fixed potential null reference in NodeHeader when accessing hardcodedValues - Improved history management in edgeStore by saving state before modifications - Significantly enhanced historyStore with better undo/redo functionality: - Added checks to prevent redundant state changes - Fixed comparison of current vs. previous states - Improved tracking of drag operations - Fixed nodeStore to properly track node position changes and maintain history - Removed console.log statement from FormRenderer ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Verified undo/redo functionality works correctly after adding/removing nodes - [x] Confirmed error handling works when validation fails - [x] Tested node dragging and collision resolution - [x] Verified edge creation and deletion properly updates history
This commit is contained in:
@@ -48,17 +48,29 @@ export const useRunInputDialog = ({
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof ApiError && error.isGraphValidationError()) {
|
||||
const errorData = error.response?.detail;
|
||||
Object.entries(errorData.node_errors).forEach(
|
||||
([nodeId, nodeErrors]) => {
|
||||
useNodeStore
|
||||
.getState()
|
||||
.updateNodeErrors(
|
||||
nodeId,
|
||||
nodeErrors as { [key: string]: string },
|
||||
);
|
||||
},
|
||||
);
|
||||
const errorData = error.response?.detail || {
|
||||
node_errors: {},
|
||||
message: undefined,
|
||||
};
|
||||
const nodeErrors = errorData.node_errors || {};
|
||||
|
||||
if (Object.keys(nodeErrors).length > 0) {
|
||||
Object.entries(nodeErrors).forEach(
|
||||
([nodeId, nodeErrorsForNode]) => {
|
||||
useNodeStore
|
||||
.getState()
|
||||
.updateNodeErrors(
|
||||
nodeId,
|
||||
nodeErrorsForNode as { [key: string]: string },
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
useNodeStore.getState().nodes.forEach((node) => {
|
||||
useNodeStore.getState().updateNodeErrors(node.id, {});
|
||||
});
|
||||
}
|
||||
|
||||
toast({
|
||||
title: errorData?.message || "Graph validation failed",
|
||||
description:
|
||||
@@ -67,7 +79,7 @@ export const useRunInputDialog = ({
|
||||
});
|
||||
setIsOpen(false);
|
||||
|
||||
const firstBackendId = Object.keys(errorData.node_errors)[0];
|
||||
const firstBackendId = Object.keys(nodeErrors)[0];
|
||||
|
||||
if (firstBackendId) {
|
||||
const firstErrorNode = useNodeStore
|
||||
|
||||
@@ -55,14 +55,16 @@ export const Flow = () => {
|
||||
const edgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
|
||||
|
||||
const onNodeDragStop = useCallback(() => {
|
||||
const currentNodes = useNodeStore.getState().nodes;
|
||||
setNodes(
|
||||
resolveCollisions(nodes, {
|
||||
resolveCollisions(currentNodes, {
|
||||
maxIterations: Infinity,
|
||||
overlapThreshold: 0.5,
|
||||
margin: 15,
|
||||
}),
|
||||
);
|
||||
}, [setNodes, nodes]);
|
||||
}, [setNodes]);
|
||||
|
||||
const { edges, onConnect, onEdgesChange } = useCustomEdge();
|
||||
|
||||
// for loading purpose
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useCallback } from "react";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useHistoryStore } from "../../../stores/historyStore";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
|
||||
export const useCustomEdge = () => {
|
||||
@@ -51,7 +52,20 @@ export const useCustomEdge = () => {
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange<CustomEdge>[]) => {
|
||||
const hasRemoval = changes.some((change) => change.type === "remove");
|
||||
|
||||
const prevState = hasRemoval
|
||||
? {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: edges,
|
||||
}
|
||||
: null;
|
||||
|
||||
setEdges(applyEdgeChanges(changes, edges));
|
||||
|
||||
if (prevState) {
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
}
|
||||
},
|
||||
[edges, setEdges],
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
const title =
|
||||
(data.metadata?.customized_name as string) ||
|
||||
data.hardcodedValues.agent_name ||
|
||||
data.hardcodedValues?.agent_name ||
|
||||
data.title;
|
||||
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
|
||||
@@ -5,6 +5,8 @@ import { customEdgeToLink, linkToCustomEdge } from "../components/helper";
|
||||
import { MarkerType } from "@xyflow/react";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||
import { useHistoryStore } from "./historyStore";
|
||||
import { useNodeStore } from "./nodeStore";
|
||||
|
||||
type EdgeStore = {
|
||||
edges: CustomEdge[];
|
||||
@@ -53,25 +55,36 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
id,
|
||||
};
|
||||
|
||||
set((state) => {
|
||||
const exists = state.edges.some(
|
||||
(e) =>
|
||||
e.source === newEdge.source &&
|
||||
e.target === newEdge.target &&
|
||||
e.sourceHandle === newEdge.sourceHandle &&
|
||||
e.targetHandle === newEdge.targetHandle,
|
||||
);
|
||||
if (exists) return state;
|
||||
return { edges: [...state.edges, newEdge] };
|
||||
});
|
||||
const exists = get().edges.some(
|
||||
(e) =>
|
||||
e.source === newEdge.source &&
|
||||
e.target === newEdge.target &&
|
||||
e.sourceHandle === newEdge.sourceHandle &&
|
||||
e.targetHandle === newEdge.targetHandle,
|
||||
);
|
||||
if (exists) return newEdge;
|
||||
const prevState = {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: get().edges,
|
||||
};
|
||||
|
||||
set((state) => ({ edges: [...state.edges, newEdge] }));
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
|
||||
return newEdge;
|
||||
},
|
||||
|
||||
removeEdge: (edgeId) =>
|
||||
removeEdge: (edgeId) => {
|
||||
const prevState = {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: get().edges,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
edges: state.edges.filter((e) => e.id !== edgeId),
|
||||
})),
|
||||
}));
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
},
|
||||
|
||||
upsertMany: (edges) =>
|
||||
set((state) => {
|
||||
|
||||
@@ -37,6 +37,15 @@ export const useHistoryStore = create<HistoryStore>((set, get) => ({
|
||||
return;
|
||||
}
|
||||
|
||||
const actualCurrentState = {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
if (isEqual(state, actualCurrentState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((prev) => ({
|
||||
past: [...prev.past.slice(-MAX_HISTORY + 1), state],
|
||||
future: [],
|
||||
@@ -55,18 +64,25 @@ export const useHistoryStore = create<HistoryStore>((set, get) => ({
|
||||
|
||||
undo: () => {
|
||||
const { past, future } = get();
|
||||
if (past.length <= 1) return;
|
||||
if (past.length === 0) return;
|
||||
|
||||
const currentState = past[past.length - 1];
|
||||
const actualCurrentState = {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
const previousState = past[past.length - 2];
|
||||
const previousState = past[past.length - 1];
|
||||
|
||||
if (isEqual(actualCurrentState, previousState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
useNodeStore.getState().setNodes(previousState.nodes);
|
||||
useEdgeStore.getState().setEdges(previousState.edges);
|
||||
|
||||
set({
|
||||
past: past.slice(0, -1),
|
||||
future: [currentState, ...future],
|
||||
past: past.length > 1 ? past.slice(0, -1) : past,
|
||||
future: [actualCurrentState, ...future],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -74,18 +90,36 @@ export const useHistoryStore = create<HistoryStore>((set, get) => ({
|
||||
const { past, future } = get();
|
||||
if (future.length === 0) return;
|
||||
|
||||
const actualCurrentState = {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
const nextState = future[0];
|
||||
|
||||
useNodeStore.getState().setNodes(nextState.nodes);
|
||||
useEdgeStore.getState().setEdges(nextState.edges);
|
||||
|
||||
const lastPast = past[past.length - 1];
|
||||
const shouldPushToPast =
|
||||
!lastPast || !isEqual(actualCurrentState, lastPast);
|
||||
|
||||
set({
|
||||
past: [...past, nextState],
|
||||
past: shouldPushToPast ? [...past, actualCurrentState] : past,
|
||||
future: future.slice(1),
|
||||
});
|
||||
},
|
||||
|
||||
canUndo: () => get().past.length > 1,
|
||||
canUndo: () => {
|
||||
const { past } = get();
|
||||
if (past.length === 0) return false;
|
||||
|
||||
const actualCurrentState = {
|
||||
nodes: useNodeStore.getState().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
return !isEqual(actualCurrentState, past[past.length - 1]);
|
||||
},
|
||||
canRedo: () => get().future.length > 0,
|
||||
|
||||
clear: () => set({ past: [{ nodes: [], edges: [] }], future: [] }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { NodeChange, XYPosition, applyNodeChanges } from "@xyflow/react";
|
||||
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode/CustomNode";
|
||||
import { CustomEdge } from "../components/FlowEditor/edges/CustomEdge";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import {
|
||||
convertBlockInfoIntoCustomNodeData,
|
||||
@@ -44,6 +45,8 @@ const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||
// Track initial positions when drag starts (outside store to avoid re-renders)
|
||||
const dragStartPositions: Record<string, XYPosition> = {};
|
||||
|
||||
let dragStartState: { nodes: CustomNode[]; edges: CustomEdge[] } | null = null;
|
||||
|
||||
type NodeStore = {
|
||||
nodes: CustomNode[];
|
||||
nodeCounter: number;
|
||||
@@ -124,14 +127,20 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
nodeCounter: state.nodeCounter + 1,
|
||||
})),
|
||||
onNodesChange: (changes) => {
|
||||
const prevState = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
// Track initial positions when drag starts
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "position" && change.dragging === true) {
|
||||
if (!dragStartState) {
|
||||
const currentNodes = get().nodes;
|
||||
const currentEdges = useEdgeStore.getState().edges;
|
||||
dragStartState = {
|
||||
nodes: currentNodes.map((n) => ({
|
||||
...n,
|
||||
position: { ...n.position },
|
||||
data: { ...n.data },
|
||||
})),
|
||||
edges: currentEdges.map((e) => ({ ...e })),
|
||||
};
|
||||
}
|
||||
if (!dragStartPositions[change.id]) {
|
||||
const node = get().nodes.find((n) => n.id === change.id);
|
||||
if (node) {
|
||||
@@ -141,12 +150,17 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we should track this change in history
|
||||
let shouldTrack = changes.some(
|
||||
(change) => change.type === "remove" || change.type === "add",
|
||||
);
|
||||
let shouldTrack = changes.some((change) => change.type === "remove");
|
||||
let stateToTrack: { nodes: CustomNode[]; edges: CustomEdge[] } | null =
|
||||
null;
|
||||
|
||||
if (shouldTrack) {
|
||||
stateToTrack = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
}
|
||||
|
||||
// For position changes, only track if movement exceeds threshold
|
||||
if (!shouldTrack) {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "position" && change.dragging === false) {
|
||||
@@ -158,20 +172,23 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
);
|
||||
if (distanceMoved > MINIMUM_MOVE_BEFORE_LOG) {
|
||||
shouldTrack = true;
|
||||
stateToTrack = dragStartState;
|
||||
}
|
||||
}
|
||||
// Clean up tracked position after drag ends
|
||||
delete dragStartPositions[change.id];
|
||||
}
|
||||
});
|
||||
if (Object.keys(dragStartPositions).length === 0) {
|
||||
dragStartState = null;
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
nodes: applyNodeChanges(changes, state.nodes),
|
||||
}));
|
||||
|
||||
if (shouldTrack) {
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
if (shouldTrack && stateToTrack) {
|
||||
useHistoryStore.getState().pushState(stateToTrack);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -185,6 +202,11 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
hardcodedValues?: Record<string, any>,
|
||||
position?: XYPosition,
|
||||
) => {
|
||||
const prevState = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
const customNodeData = convertBlockInfoIntoCustomNodeData(
|
||||
block,
|
||||
hardcodedValues,
|
||||
@@ -218,21 +240,24 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
set((state) => ({
|
||||
nodes: [...state.nodes, customNode],
|
||||
}));
|
||||
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
|
||||
return customNode;
|
||||
},
|
||||
updateNodeData: (nodeId, data) => {
|
||||
const prevState = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n,
|
||||
),
|
||||
}));
|
||||
|
||||
const newState = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
useHistoryStore.getState().pushState(newState);
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
},
|
||||
toggleAdvanced: (nodeId: string) =>
|
||||
set((state) => ({
|
||||
@@ -391,6 +416,11 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
},
|
||||
|
||||
setCredentialsOptional: (nodeId: string, optional: boolean) => {
|
||||
const prevState = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId
|
||||
@@ -408,12 +438,7 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const newState = {
|
||||
nodes: get().nodes,
|
||||
edges: useEdgeStore.getState().edges,
|
||||
};
|
||||
|
||||
useHistoryStore.getState().pushState(newState);
|
||||
useHistoryStore.getState().pushState(prevState);
|
||||
},
|
||||
|
||||
// Sub-agent resolution mode state
|
||||
|
||||
@@ -30,8 +30,6 @@ export const FormRenderer = ({
|
||||
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
|
||||
}, [preprocessedSchema, uiSchema]);
|
||||
|
||||
console.log("preprocessedSchema", preprocessedSchema);
|
||||
|
||||
return (
|
||||
<div className={"mb-6 mt-4"} data-tutorial-id="input-handles">
|
||||
<Form
|
||||
|
||||
Reference in New Issue
Block a user