diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts index 2b37ebe8a9..db3b6660df 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts @@ -9,6 +9,8 @@ import { useGraphStore } from "@/app/(platform)/build/stores/graphStore"; import { useShallow } from "zustand/react/shallow"; import { useState } from "react"; import { useSaveGraph } from "@/app/(platform)/build/hooks/useSaveGraph"; +import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore"; +import { ApiError } from "@/lib/autogpt-server-api/helpers"; // Check if this exists export const useRunGraph = () => { const { saveGraph, isSaving } = useSaveGraph({ @@ -24,6 +26,13 @@ export const useRunGraph = () => { ); const [openRunInputDialog, setOpenRunInputDialog] = useState(false); + const setNodeErrorsForBackendId = useNodeStore( + useShallow((state) => state.setNodeErrorsForBackendId), + ); + const clearAllNodeErrors = useNodeStore( + useShallow((state) => state.clearAllNodeErrors), + ); + const [{ flowID, flowVersion, flowExecutionID }, setQueryStates] = useQueryStates({ flowID: parseAsString, @@ -35,19 +44,49 @@ export const useRunGraph = () => { usePostV1ExecuteGraphAgent({ mutation: { onSuccess: (response: any) => { + clearAllNodeErrors(); const { id } = response.data as GraphExecutionMeta; setQueryStates({ flowExecutionID: id, }); }, onError: (error: any) => { - // Reset running state on error setIsGraphRunning(false); - toast({ - title: (error.detail as string) ?? "An unexpected error occurred.", - description: "An unexpected error occurred.", - variant: "destructive", - }); + if (error instanceof ApiError && error.isGraphValidationError?.()) { + const errorData = error.response?.detail; + + if (errorData?.node_errors) { + Object.entries(errorData.node_errors).forEach( + ([backendId, nodeErrors]) => { + setNodeErrorsForBackendId( + backendId, + nodeErrors as { [key: string]: string }, + ); + }, + ); + + useNodeStore.getState().nodes.forEach((node) => { + const backendId = node.data.metadata?.backend_id || node.id; + if (!errorData.node_errors[backendId as string]) { + useNodeStore.getState().updateNodeErrors(node.id, {}); + } + }); + } + + toast({ + title: errorData?.message || "Graph validation failed", + description: + "Please fix the validation errors on the highlighted nodes and try again.", + variant: "destructive", + }); + } else { + toast({ + title: + (error.detail as string) ?? "An unexpected error occurred.", + description: "An unexpected error occurred.", + variant: "destructive", + }); + } }, }, }); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useFlow.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useFlow.ts index badb9784b8..64f00871d8 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useFlow.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/useFlow.ts @@ -81,7 +81,7 @@ export const useFlow = () => { { query: { select: (res) => res.data as BlockInfo[], - enabled: !!flowID && !!blockIds, + enabled: !!flowID && !!blockIds && blockIds.length > 0, }, }, ); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx index 52df3edbc4..974cbe3754 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx @@ -37,6 +37,7 @@ export type CustomNodeData = { costs: BlockCost[]; categories: BlockInfoCategoriesItem[]; metadata?: NodeModelMetadata; + errors?: { [key: string]: string }; }; export type CustomNode = XYNode; @@ -71,10 +72,24 @@ export const CustomNode: React.FC> = React.memo( ? (data.hardcodedValues.output_schema ?? {}) : data.outputSchema; + const hasConfigErrors = + data.errors && + Object.values(data.errors).some( + (value) => value !== null && value !== undefined && value !== "", + ); + + const outputData = data.nodeExecutionResult?.output_data; + const hasOutputError = + typeof outputData === "object" && + outputData !== null && + "error" in outputData; + + const hasErrors = hasConfigErrors || hasOutputError; + // Currently all blockTypes design are similar - that's why i am using the same component for all of them // If in future - if we need some drastic change in some blockTypes design - we can create separate components for them return ( - +
{isWebhook && } diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContainer.tsx index 657f1ca048..f8d5b2e089 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContainer.tsx @@ -3,15 +3,18 @@ import { nodeStyleBasedOnStatus } from "../helpers"; import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore"; import { useShallow } from "zustand/react/shallow"; +import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus"; export const NodeContainer = ({ children, nodeId, selected, + hasErrors, // these are configuration errors that occur before executing the graph -- more like validation errors }: { children: React.ReactNode; nodeId: string; selected: boolean; + hasErrors?: boolean; }) => { const status = useNodeStore( useShallow((state) => state.getNodeStatus(nodeId)), @@ -22,6 +25,7 @@ export const NodeContainer = ({ "z-12 max-w-[370px] rounded-xlarge ring-1 ring-slate-200/60", selected && "shadow-lg ring-2 ring-slate-200", status && nodeStyleBasedOnStatus[status], + hasErrors ? nodeStyleBasedOnStatus[AgentExecutionStatus.FAILED] : "", )} > {children} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts index 3beba0c615..2f41c3bb46 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/nodeStore.ts @@ -53,6 +53,15 @@ type NodeStore = { getNodeExecutionResult: (nodeId: string) => NodeExecutionResult | undefined; getNodeBlockUIType: (nodeId: string) => BlockUIType; hasWebhookNodes: () => boolean; + + updateNodeErrors: (nodeId: string, errors: { [key: string]: string }) => void; + clearNodeErrors: (nodeId: string) => void; + getNodeErrors: (nodeId: string) => { [key: string]: string } | undefined; + setNodeErrorsForBackendId: ( + backendId: string, + errors: { [key: string]: string }, + ) => void; + clearAllNodeErrors: () => void; // Add this }; export const useNodeStore = create((set, get) => ({ @@ -253,4 +262,47 @@ export const useNodeStore = create((set, get) => ({ [BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(n.data.uiType), ); }, + + updateNodeErrors: (nodeId: string, errors: { [key: string]: string }) => { + set((state) => ({ + nodes: state.nodes.map((n) => + n.id === nodeId ? { ...n, data: { ...n.data, errors } } : n, + ), + })); + }, + + clearNodeErrors: (nodeId: string) => { + set((state) => ({ + nodes: state.nodes.map((n) => + n.id === nodeId ? { ...n, data: { ...n.data, errors: undefined } } : n, + ), + })); + }, + + getNodeErrors: (nodeId: string) => { + return get().nodes.find((n) => n.id === nodeId)?.data?.errors; + }, + + setNodeErrorsForBackendId: ( + backendId: string, + errors: { [key: string]: string }, + ) => { + set((state) => ({ + nodes: state.nodes.map((n) => { + // Match by backend_id if nodes have it, or by id + const matches = + n.data.metadata?.backend_id === backendId || n.id === backendId; + return matches ? { ...n, data: { ...n.data, errors } } : n; + }), + })); + }, + + clearAllNodeErrors: () => { + set((state) => ({ + nodes: state.nodes.map((n) => ({ + ...n, + data: { ...n.data, errors: undefined }, + })), + })); + }, })); diff --git a/autogpt_platform/frontend/src/components/renderers/input-renderer/templates/FieldTemplate.tsx b/autogpt_platform/frontend/src/components/renderers/input-renderer/templates/FieldTemplate.tsx index b4db9d4159..a056782939 100644 --- a/autogpt_platform/frontend/src/components/renderers/input-renderer/templates/FieldTemplate.tsx +++ b/autogpt_platform/frontend/src/components/renderers/input-renderer/templates/FieldTemplate.tsx @@ -23,6 +23,7 @@ import { cn } from "@/lib/utils"; import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api"; import { BlockUIType } from "@/lib/autogpt-server-api"; import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle"; +import { getFieldErrorKey } from "../utils/helpers"; const FieldTemplate: React.FC = ({ id: fieldId, @@ -42,6 +43,11 @@ const FieldTemplate: React.FC = ({ (state) => state.nodeAdvancedStates[nodeId] ?? false, ); + const nodeErrors = useNodeStore((state) => { + const node = state.nodes.find((n) => n.id === nodeId); + return node?.data?.errors; + }); + const { isArrayItem, arrayFieldHandleId } = useContext(ArrayEditorContext); const isAnyOf = @@ -89,6 +95,13 @@ const FieldTemplate: React.FC = ({ shouldShowHandle = false; } + const fieldErrorKey = getFieldErrorKey(fieldId); + const fieldError = + nodeErrors?.[fieldErrorKey] || + nodeErrors?.[fieldErrorKey.replace(/_/g, ".")] || + nodeErrors?.[fieldErrorKey.replace(/\./g, "_")] || + null; + return (
= ({
{children}
- )}{" "} + )} + {fieldError && ( + + {fieldError} + + )}
); }; diff --git a/autogpt_platform/frontend/src/components/renderers/input-renderer/utils/helpers.ts b/autogpt_platform/frontend/src/components/renderers/input-renderer/utils/helpers.ts new file mode 100644 index 0000000000..51b628d923 --- /dev/null +++ b/autogpt_platform/frontend/src/components/renderers/input-renderer/utils/helpers.ts @@ -0,0 +1,4 @@ +export const getFieldErrorKey = (fieldId: string): string => { + const withoutRoot = fieldId.startsWith("root_") ? fieldId.slice(5) : fieldId; + return withoutRoot; +};