feat(frontend): add extra info in custom node in new builder (#11172)

Currently, we don’t add category and cost information to custom nodes in
the new builder. This means we’re rendering with the correct information
and costs are displayed accurately based on the selected discriminator
value.

<img width="441" height="781" alt="Screenshot 2025-10-15 at 2 43 33 PM"
src="https://github.com/user-attachments/assets/8199cfa7-4353-4de2-8c15-b68aa86e458c"
/>


### 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] All information is displayed correctly.
- [x] I’ve tried changing the discrimination value and we’re getting the
correct cost for the selected value.
This commit is contained in:
Abhimanyu Yadav
2025-10-17 10:05:22 +05:30
committed by GitHub
parent 9469b9e2eb
commit f3f9a60157
10 changed files with 229 additions and 90 deletions

View File

@@ -5,7 +5,7 @@ import { useFlow } from "./useFlow";
import { useShallow } from "zustand/react/shallow";
import { useNodeStore } from "../../../stores/nodeStore";
import { useMemo } from "react";
import { CustomNode } from "../nodes/CustomNode";
import { CustomNode } from "../nodes/CustomNode/CustomNode";
import { useCustomEdge } from "../edges/useCustomEdge";
import { GraphLoadingBox } from "./GraphLoadingBox";

View File

@@ -1,84 +0,0 @@
import React from "react";
import { Node as XYNode, NodeProps } from "@xyflow/react";
import { FormCreator } from "./FormCreator";
import { RJSFSchema } from "@rjsf/utils";
import { Text } from "@/components/atoms/Text/Text";
import { Switch } from "@/components/atoms/Switch/Switch";
import { preprocessInputSchema } from "../processors/input-schema-pre-processor";
import { OutputHandler } from "./OutputHandler";
import { useNodeStore } from "../../../stores/nodeStore";
import { cn } from "@/lib/utils";
import { BlockUIType } from "../../types";
import { StickyNoteBlock } from "./StickyNoteBlock";
export type CustomNodeData = {
hardcodedValues: {
[key: string]: any;
};
title: string;
block_id: string;
description: string;
inputSchema: RJSFSchema;
outputSchema: RJSFSchema;
uiType: BlockUIType;
};
export type CustomNode = XYNode<CustomNodeData, "custom">;
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
({ data, id: nodeId, selected }) => {
const showAdvanced = useNodeStore(
(state) => state.nodeAdvancedStates[nodeId] || false,
);
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
if (data.uiType === BlockUIType.NOTE) {
return <StickyNoteBlock selected={selected} data={data} id={nodeId} />;
}
return (
<div
className={cn(
"z-12 rounded-xl bg-gradient-to-br from-white to-slate-50/30 shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
selected && "shadow-2xl ring-2 ring-slate-200",
)}
>
{/* Header */}
<div className="flex h-14 items-center justify-center rounded-xl border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90">
<Text
variant="large-semibold"
className="tracking-tight text-slate-800"
>
{data.title}
</Text>
</div>
{/* Input Handles */}
<div className="bg-white/40 pb-6 pr-6">
<FormCreator
jsonSchema={preprocessInputSchema(data.inputSchema)}
nodeId={nodeId}
uiType={data.uiType}
/>
</div>
{/* Advanced Button */}
<div className="flex items-center justify-between gap-2 rounded-b-xl border-t border-slate-200/50 bg-gradient-to-r from-slate-50/60 to-white/80 px-5 py-3.5">
<Text variant="body" className="font-medium text-slate-700">
Advanced
</Text>
<Switch
onCheckedChange={(checked) => setShowAdvanced(nodeId, checked)}
checked={showAdvanced}
/>
</div>
{/* Output Handles */}
<OutputHandler outputSchema={data.outputSchema} nodeId={nodeId} />
</div>
);
},
);
CustomNode.displayName = "CustomNode";

View File

@@ -0,0 +1,45 @@
import React from "react";
import { Node as XYNode, NodeProps } from "@xyflow/react";
import { RJSFSchema } from "@rjsf/utils";
import { BlockUIType } from "../../../types";
import { StickyNoteBlock } from "./StickyNoteBlock";
import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem";
import { StandardNodeBlock } from "./StandardNodeBlock";
import { BlockCost } from "@/app/api/__generated__/models/blockCost";
export type CustomNodeData = {
hardcodedValues: {
[key: string]: any;
};
title: string;
description: string;
inputSchema: RJSFSchema;
outputSchema: RJSFSchema;
uiType: BlockUIType;
block_id: string;
// TODO : We need better type safety for the following backend fields.
costs: BlockCost[];
categories: BlockInfoCategoriesItem[];
};
export type CustomNode = XYNode<CustomNodeData, "custom">;
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
({ data, id: nodeId, selected }) => {
if (data.uiType === BlockUIType.NOTE) {
return <StickyNoteBlock selected={selected} data={data} id={nodeId} />;
}
if (data.uiType === BlockUIType.STANDARD) {
return (
<StandardNodeBlock data={data} selected={selected} nodeId={nodeId} />
);
}
return (
<StandardNodeBlock data={data} selected={selected} nodeId={nodeId} />
);
},
);
CustomNode.displayName = "CustomNode";

View File

@@ -0,0 +1,79 @@
import { beautifyString, cn } from "@/lib/utils";
import { CustomNodeData } from "./CustomNode";
import { Text } from "@/components/atoms/Text/Text";
import { FormCreator } from "../FormCreator";
import { preprocessInputSchema } from "../../processors/input-schema-pre-processor";
import { Switch } from "@/components/atoms/Switch/Switch";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { OutputHandler } from "../OutputHandler";
import { NodeCost } from "./components/NodeCost";
import { NodeBadges } from "./components/NodeBadges";
type StandardNodeBlockType = {
data: CustomNodeData;
selected: boolean;
nodeId: string;
};
export const StandardNodeBlock = ({
data,
selected,
nodeId,
}: StandardNodeBlockType) => {
const showAdvanced = useNodeStore(
(state) => state.nodeAdvancedStates[nodeId] || false,
);
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
return (
<div
className={cn(
"z-12 rounded-xl bg-gradient-to-br from-white to-slate-50/30 shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
selected && "shadow-2xl ring-2 ring-slate-200",
)}
>
{/* Header */}
<div className="flex h-auto flex-col gap-2 rounded-xl border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4">
{/* Upper section */}
<div className="flex items-center gap-2">
<Text
variant="large-semibold"
className="tracking-tight text-slate-800"
>
{beautifyString(data.title)}
</Text>
<Text variant="small" className="!font-medium !text-slate-500">
#{nodeId.split("-")[0]}
</Text>
</div>
{/* Lower section */}
<div className="flex space-x-2">
<NodeCost blockCosts={data.costs} nodeId={nodeId} />
<NodeBadges categories={data.categories} />
</div>
</div>
{/* Input Handles */}
<div className="bg-white/40 pb-6 pr-6">
<FormCreator
jsonSchema={preprocessInputSchema(data.inputSchema)}
nodeId={nodeId}
uiType={data.uiType}
/>
</div>
{/* Advanced Button */}
<div className="flex items-center justify-between gap-2 rounded-b-xl border-t border-slate-200/50 bg-gradient-to-r from-slate-50/60 to-white/80 px-5 py-3.5">
<Text variant="body" className="font-medium text-slate-700">
Advanced
</Text>
<Switch
onCheckedChange={(checked) => setShowAdvanced(nodeId, checked)}
checked={showAdvanced}
/>
</div>
{/* Output Handles */}
<OutputHandler outputSchema={data.outputSchema} nodeId={nodeId} />
</div>
);
};

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { FormCreator } from "./FormCreator";
import { preprocessInputSchema } from "../processors/input-schema-pre-processor";
import { FormCreator } from "../FormCreator";
import { preprocessInputSchema } from "../../processors/input-schema-pre-processor";
import { CustomNodeData } from "./CustomNode";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";

View File

@@ -0,0 +1,20 @@
import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem";
import { Badge } from "@/components/__legacy__/ui/badge";
import { beautifyString, cn } from "@/lib/utils";
export const NodeBadges = ({
categories,
}: {
categories: BlockInfoCategoriesItem[];
}) => {
return categories.map((category) => (
<Badge
key={category.category}
className={cn(
"rounded-full border border-slate-500 bg-slate-100 text-black shadow-none",
)}
>
{beautifyString(category.category.toLowerCase())}
</Badge>
));
};

View File

@@ -0,0 +1,39 @@
import { BlockCost } from "@/app/api/__generated__/models/blockCost";
import { Text } from "@/components/atoms/Text/Text";
import useCredits from "@/hooks/useCredits";
import { CoinIcon } from "@phosphor-icons/react";
import { isCostFilterMatch } from "../../../../helper";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
export const NodeCost = ({
blockCosts,
nodeId,
}: {
blockCosts: BlockCost[];
nodeId: string;
}) => {
const { formatCredits } = useCredits();
const hardcodedValues = useNodeStore((state) =>
state.getHardCodedValues(nodeId),
);
const blockCost =
blockCosts &&
blockCosts.find((cost) =>
isCostFilterMatch(cost.cost_filter, hardcodedValues),
);
if (!blockCost) return null;
return (
<div className="mr-3 flex items-center gap-1 text-base font-light">
<CoinIcon className="h-3 w-3" />
<Text variant="small" className="!font-medium">
{formatCredits(blockCost.cost_amount)}
</Text>
<Text variant="small">
{" \/"}
{blockCost.cost_type}
</Text>
</div>
);
};

View File

@@ -8,11 +8,11 @@ import { GraphExecutionID } from "@/lib/autogpt-server-api";
// import { ControlPanelButton } from "../ControlPanelButton";
import { ArrowUUpLeftIcon, ArrowUUpRightIcon } from "@phosphor-icons/react";
// import { GraphSearchMenu } from "../GraphMenu/GraphMenu";
import { CustomNode } from "../FlowEditor/nodes/CustomNode";
import { history } from "@/app/(platform)/build/components/legacy-builder/history";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { Separator } from "@/components/__legacy__/ui/separator";
import { NewSaveControl } from "./NewSaveControl/NewSaveControl";
import { CustomNode } from "../FlowEditor/nodes/CustomNode/CustomNode";
export type Control = {
icon: React.ReactNode;

View File

@@ -1,5 +1,8 @@
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
import { CustomNode, CustomNodeData } from "./FlowEditor/nodes/CustomNode";
import {
CustomNode,
CustomNodeData,
} from "./FlowEditor/nodes/CustomNode/CustomNode";
import { BlockUIType } from "./types";
import { NodeModel } from "@/app/api/__generated__/models/nodeModel";
import { NodeModelMetadata } from "@/app/api/__generated__/models/nodeModelMetadata";
@@ -14,8 +17,10 @@ export const convertBlockInfoIntoCustomNodeData = (
description: block.description,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
categories: block.categories,
uiType: block.uiType as BlockUIType,
block_id: block.id,
costs: block.costs,
};
return customNodeData;
};
@@ -51,3 +56,38 @@ export const convertNodesPlusBlockInfoIntoCustomNodes = (
};
return customNode;
};
export enum BlockCategory {
AI = "AI",
SOCIAL = "SOCIAL",
TEXT = "TEXT",
SEARCH = "SEARCH",
BASIC = "BASIC",
INPUT = "INPUT",
OUTPUT = "OUTPUT",
LOGIC = "LOGIC",
COMMUNICATION = "COMMUNICATION",
DEVELOPER_TOOLS = "DEVELOPER_TOOLS",
DATA = "DATA",
HARDWARE = "HARDWARE",
AGENT = "AGENT",
CRM = "CRM",
SAFETY = "SAFETY",
PRODUCTIVITY = "PRODUCTIVITY",
ISSUE_TRACKING = "ISSUE_TRACKING",
MULTIMEDIA = "MULTIMEDIA",
MARKETING = "MARKETING",
}
// Cost related helpers
export const isCostFilterMatch = (
costFilter: any,
inputValues: any,
): boolean => {
return typeof costFilter === "object" && typeof inputValues === "object"
? Object.entries(costFilter).every(
([k, v]) =>
(!v && !inputValues[k]) || isCostFilterMatch(v, inputValues[k]),
)
: costFilter === inputValues;
};

View File

@@ -1,6 +1,6 @@
import { create } from "zustand";
import { NodeChange, applyNodeChanges } from "@xyflow/react";
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode";
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode/CustomNode";
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
import { convertBlockInfoIntoCustomNodeData } from "../components/helper";
import { Node } from "@/app/api/__generated__/models/node";