mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
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:
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
));
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user