mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
refactor(frontend): improve customNode reusability and add multi-block support (#11368)
This refactor improves developer experience (DX) by creating a more maintainable and extensible architecture. The previous `CustomNode` implementation had several issues: - Code was duplicated across different node types (StandardNodeBlock, OutputBlock, etc.) - Poor separation of concerns with all logic in a single component - Limited flexibility for handling different block types - Inconsistent handle display logic across different node types <img width="2133" height="831" alt="Screenshot 2025-11-12 at 9 25 10 PM" src="https://github.com/user-attachments/assets/02864bba-9ffe-4629-98ab-1c43fa644844" /> ## Changes 🏗️ - **Refactored CustomNode structure**: - Extracted reusable components: [`NodeContainer`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContainer.tsx), [`NodeHeader`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeHeader.tsx), [`NodeAdvancedToggle`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeAdvancedToggle.tsx), [`WebhookDisclaimer`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/WebhookDisclaimer.tsx) - Removed `StandardNodeBlock.tsx` and consolidated logic into [`CustomNode.tsx`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx) - Moved [`StickyNoteBlock`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/StickyNoteBlock.tsx) to components folder for better organization - **Added BlockUIType-specific logic**: - Implemented conditional handle display based on block type (INPUT, WEBHOOK, WEBHOOK_MANUAL blocks don't show handles) - Added special handling for AGENT blocks with dynamic input/output schemas - Added webhook-specific disclaimer component with library agent integration - Fixed OUTPUT block's name field to not show input handle - **Enhanced FormCreator**: - Added `showHandles` prop for granular control - Added `className` prop for styling flexibility (used for webhook opacity) - **Improved nodeStore**: - Added `getNodeBlockUIType` method for retrieving node UI types - **UI/UX improvements**: - Fixed duplicate gap classes in [`BuilderActions`](file:///Users/abhi/Documents/AutoGPT/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/BuilderActions.tsx) - Added proper styling for webhook blocks (disabled state with reduced opacity) - Improved field template spacing for specific block types ## 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] Create and test a standard node block with input/output handles - [x] Create and test INPUT block (verify no input handles) - [x] Create and test OUTPUT block (verify name field has no handle) - [x] Create and test WEBHOOK block (verify disclaimer appears and form is disabled) - [x] Create and test AGENT block with custom schemas - [x] Create and test sticky note block - [x] Verify advanced toggle works for all node types - [x] Test node execution badges display correctly - [x] Verify node selection highlighting works
This commit is contained in:
@@ -4,7 +4,7 @@ import { ScheduleGraph } from "./components/ScheduleGraph/ScheduleGraph";
|
||||
|
||||
export const BuilderActions = () => {
|
||||
return (
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-2 gap-4">
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4">
|
||||
<AgentOutputs />
|
||||
<RunGraph />
|
||||
<ScheduleGraph />
|
||||
|
||||
@@ -2,12 +2,22 @@ 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 { StickyNoteBlock } from "./components/StickyNoteBlock";
|
||||
import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem";
|
||||
import { StandardNodeBlock } from "./StandardNodeBlock";
|
||||
import { BlockCost } from "@/app/api/__generated__/models/blockCost";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { NodeContainer } from "./components/NodeContainer";
|
||||
import { NodeHeader } from "./components/NodeHeader";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle";
|
||||
import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
||||
import { NodeExecutionBadge } from "./components/NodeExecutionBadge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||
import { AyrshareConnectButton } from "./components/AyrshareConnectButton";
|
||||
|
||||
export type CustomNodeData = {
|
||||
hardcodedValues: {
|
||||
@@ -32,17 +42,59 @@ 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} />
|
||||
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
|
||||
);
|
||||
}
|
||||
|
||||
const showHandles =
|
||||
data.uiType !== BlockUIType.INPUT &&
|
||||
data.uiType !== BlockUIType.WEBHOOK &&
|
||||
data.uiType !== BlockUIType.WEBHOOK_MANUAL;
|
||||
|
||||
const isWebhook = [
|
||||
BlockUIType.WEBHOOK,
|
||||
BlockUIType.WEBHOOK_MANUAL,
|
||||
].includes(data.uiType);
|
||||
|
||||
const isAyrshare = data.uiType === BlockUIType.AYRSHARE;
|
||||
|
||||
const inputSchema =
|
||||
data.uiType === BlockUIType.AGENT
|
||||
? (data.hardcodedValues.input_schema ?? {})
|
||||
: data.inputSchema;
|
||||
|
||||
const outputSchema =
|
||||
data.uiType === BlockUIType.AGENT
|
||||
? (data.hardcodedValues.output_schema ?? {})
|
||||
: data.outputSchema;
|
||||
|
||||
// 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 (
|
||||
<StandardNodeBlock data={data} selected={selected} nodeId={nodeId} />
|
||||
<NodeContainer selected={selected} nodeId={nodeId}>
|
||||
<div className="rounded-xlarge bg-white">
|
||||
<NodeHeader data={data} nodeId={nodeId} />
|
||||
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
|
||||
{isAyrshare && <AyrshareConnectButton />}
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(inputSchema)}
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
className={cn(
|
||||
"bg-white pr-6",
|
||||
isWebhook && "pointer-events-none opacity-50",
|
||||
)}
|
||||
showHandles={showHandles}
|
||||
/>
|
||||
<NodeAdvancedToggle nodeId={nodeId} />
|
||||
{data.uiType != BlockUIType.OUTPUT && (
|
||||
<OutputHandler outputSchema={outputSchema} nodeId={nodeId} />
|
||||
)}
|
||||
<NodeDataRenderer nodeId={nodeId} />
|
||||
</div>
|
||||
<NodeExecutionBadge nodeId={nodeId} />
|
||||
</NodeContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { CustomNodeData } from "./CustomNode";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/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";
|
||||
import { NodeExecutionBadge } from "./components/NodeExecutionBadge";
|
||||
import { nodeStyleBasedOnStatus } from "./helpers";
|
||||
import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
||||
import { NodeContextMenu } from "./components/NodeContextMenu";
|
||||
|
||||
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);
|
||||
const status = useNodeStore((state) => state.getNodeStatus(nodeId));
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-12 max-w-[370px] rounded-xlarge shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
|
||||
selected && "shadow-2xl ring-2 ring-slate-200",
|
||||
status && nodeStyleBasedOnStatus[status],
|
||||
)}
|
||||
>
|
||||
<div className="rounded-xlarge bg-white">
|
||||
{/* Header */}
|
||||
<div className="flex h-auto items-start justify-between gap-2 rounded-xlarge border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* 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>
|
||||
<NodeContextMenu
|
||||
subGraphID={data.hardcodedValues?.graph_id}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
</div>
|
||||
{/* Input Handles */}
|
||||
<div className="bg-white pr-6">
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(data.inputSchema)}
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
/>
|
||||
</div>
|
||||
{/* Advanced Button */}
|
||||
<div className="flex items-center justify-between gap-2 border-t border-slate-200/50 bg-white 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} />
|
||||
|
||||
<NodeDataRenderer nodeId={nodeId} />
|
||||
</div>
|
||||
{status && <NodeExecutionBadge status={status} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Key } from "lucide-react";
|
||||
import { getV1GetAyrshareSsoUrl } from "@/app/api/__generated__/endpoints/integrations/integrations";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
// This SSO button is not a part of inputSchema - that's why we are not rendering it using Input renderer
|
||||
export const AyrshareConnectButton = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleSSOLogin = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { data, status } = await getV1GetAyrshareSsoUrl();
|
||||
if (status !== 200) {
|
||||
throw new Error(data.detail);
|
||||
}
|
||||
const popup = window.open(data.sso_url, "_blank", "popup=true");
|
||||
if (!popup) {
|
||||
throw new Error(
|
||||
"Please allow popups for this site to be able to login with Ayrshare",
|
||||
);
|
||||
}
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Please complete the authentication in the popup window",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Error getting SSO URL: ${error}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
// TODO :Need better UI to show user which social media accounts are connected
|
||||
<div className="mt-4 flex flex-col gap-2 px-4">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSSOLogin}
|
||||
disabled={isLoading}
|
||||
className="h-fit w-full py-2"
|
||||
loading={isLoading}
|
||||
leftIcon={<Key className="mr-2 h-4 w-4" />}
|
||||
>
|
||||
Connect Social Media Accounts
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export const NodeAdvancedToggle = ({ nodeId }: { nodeId: string }) => {
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[nodeId] || false,
|
||||
);
|
||||
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 rounded-b-xlarge border-t border-slate-200/50 bg-white 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { nodeStyleBasedOnStatus } from "../helpers";
|
||||
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
|
||||
export const NodeContainer = ({
|
||||
children,
|
||||
nodeId,
|
||||
selected,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
nodeId: string;
|
||||
selected: boolean;
|
||||
}) => {
|
||||
const status = useNodeStore((state) => state.getNodeStatus(nodeId));
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-12 max-w-[370px] rounded-xlarge shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
|
||||
selected && "shadow-2xl ring-2 ring-slate-200",
|
||||
status && nodeStyleBasedOnStatus[status],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { Badge } from "@/components/__legacy__/ui/badge";
|
||||
import { LoadingSpinner } from "@/components/__legacy__/ui/loading";
|
||||
@@ -12,11 +13,9 @@ const statusStyles: Record<AgentExecutionStatus, string> = {
|
||||
FAILED: "text-red-700 border-red-400",
|
||||
};
|
||||
|
||||
export const NodeExecutionBadge = ({
|
||||
status,
|
||||
}: {
|
||||
status: AgentExecutionStatus;
|
||||
}) => {
|
||||
export const NodeExecutionBadge = ({ nodeId }: { nodeId: string }) => {
|
||||
const status = useNodeStore((state) => state.getNodeStatus(nodeId));
|
||||
if (!status) return null;
|
||||
return (
|
||||
<div className="flex items-center justify-end rounded-b-xl py-2 pr-4">
|
||||
<Badge
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { NodeCost } from "./NodeCost";
|
||||
import { NodeBadges } from "./NodeBadges";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
|
||||
export const NodeHeader = ({
|
||||
data,
|
||||
nodeId,
|
||||
}: {
|
||||
data: CustomNodeData;
|
||||
nodeId: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-auto items-start justify-between gap-2 rounded-xlarge border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* 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>
|
||||
<NodeContextMenu
|
||||
subGraphID={data.hardcodedValues?.graph_id}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { FormCreator } from "../../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { CustomNodeData } from "./CustomNode";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type StickyNoteBlockType = {
|
||||
selected: boolean;
|
||||
data: CustomNodeData;
|
||||
id: string;
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
export const StickyNoteBlock = ({ data, id }: StickyNoteBlockType) => {
|
||||
export const StickyNoteBlock = ({ data, nodeId }: StickyNoteBlockType) => {
|
||||
const { angle, color } = useMemo(() => {
|
||||
const hash = id.split("").reduce((acc, char) => {
|
||||
const hash = nodeId.split("").reduce((acc, char) => {
|
||||
return char.charCodeAt(0) + ((acc << 5) - acc);
|
||||
}, 0);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const StickyNoteBlock = ({ data, id }: StickyNoteBlockType) => {
|
||||
angle: (hash % 7) - 3,
|
||||
color: colors[Math.abs(hash) % colors.length],
|
||||
};
|
||||
}, [id]);
|
||||
}, [nodeId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -42,11 +42,11 @@ export const StickyNoteBlock = ({ data, id }: StickyNoteBlockType) => {
|
||||
style={{ transform: `rotate(${angle}deg)` }}
|
||||
>
|
||||
<Text variant="h3" className="tracking-tight text-slate-800">
|
||||
Notes #{id.split("-")[0]}
|
||||
Notes #{nodeId.split("-")[0]}
|
||||
</Text>
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(data.inputSchema)}
|
||||
nodeId={id}
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import Link from "next/link";
|
||||
import { useGetV2GetLibraryAgentByGraphId } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useQueryStates, parseAsString } from "nuqs";
|
||||
import { isValidUUID } from "@/app/(platform)/chat/helpers";
|
||||
|
||||
export const WebhookDisclaimer = ({ nodeId }: { nodeId: string }) => {
|
||||
const [{ flowID }] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
});
|
||||
|
||||
// for a single agentId, we are fetching everything - need to make it better in the future
|
||||
const { data: libraryAgent } = useGetV2GetLibraryAgentByGraphId(
|
||||
flowID ?? "",
|
||||
{},
|
||||
{
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as LibraryAgent;
|
||||
},
|
||||
enabled: !!flowID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const isNodeSaved = isValidUUID(nodeId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 pt-4">
|
||||
<Alert className="mb-3 rounded-xlarge">
|
||||
<AlertDescription>
|
||||
<Text variant="small-medium">
|
||||
You can set up and manage this trigger in your{" "}
|
||||
<Link
|
||||
href={
|
||||
libraryAgent
|
||||
? `/library/agents/${libraryAgent.id}`
|
||||
: "/library"
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Agent Library
|
||||
</Link>
|
||||
{!isNodeSaved && " (after saving the graph)"}.
|
||||
</Text>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<Text variant="small" className="mb-4 ml-6 !text-purple-700">
|
||||
Below inputs are only for display purposes and cannot be edited.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -10,10 +10,14 @@ export const FormCreator = React.memo(
|
||||
jsonSchema,
|
||||
nodeId,
|
||||
uiType,
|
||||
showHandles = true,
|
||||
className,
|
||||
}: {
|
||||
jsonSchema: RJSFSchema;
|
||||
nodeId: string;
|
||||
uiType: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
const getHardCodedValues = useNodeStore(
|
||||
@@ -29,18 +33,20 @@ export const FormCreator = React.memo(
|
||||
const initialValues = getHardCodedValues(nodeId);
|
||||
|
||||
return (
|
||||
<FormRenderer
|
||||
jsonSchema={jsonSchema}
|
||||
handleChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={initialValues}
|
||||
formContext={{
|
||||
nodeId: nodeId,
|
||||
uiType: uiType,
|
||||
showHandles: true,
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
<div className={className}>
|
||||
<FormRenderer
|
||||
jsonSchema={jsonSchema}
|
||||
handleChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={initialValues}
|
||||
formContext={{
|
||||
nodeId: nodeId,
|
||||
uiType: uiType,
|
||||
showHandles: showHandles,
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { useHistoryStore } from "./historyStore";
|
||||
import { useEdgeStore } from "./edgeStore";
|
||||
import { BlockUIType } from "../components/types";
|
||||
|
||||
type NodeStore = {
|
||||
nodes: CustomNode[];
|
||||
@@ -35,6 +36,8 @@ type NodeStore = {
|
||||
result: NodeExecutionResult,
|
||||
) => void;
|
||||
getNodeExecutionResult: (nodeId: string) => NodeExecutionResult | undefined;
|
||||
|
||||
getNodeBlockUIType: (nodeId: string) => BlockUIType;
|
||||
};
|
||||
|
||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
@@ -164,4 +167,10 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
getNodeExecutionResult: (nodeId: string) => {
|
||||
return get().nodes.find((n) => n.id === nodeId)?.data?.nodeExecutionResult;
|
||||
},
|
||||
getNodeBlockUIType: (nodeId: string) => {
|
||||
return (
|
||||
get().nodes.find((n) => n.id === nodeId)?.data?.uiType ??
|
||||
BlockUIType.STANDARD
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -36,6 +36,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
}) => {
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId, showHandles = true, size = "small" } = formContext;
|
||||
const uiType = formContext.uiType;
|
||||
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[nodeId] ?? false,
|
||||
@@ -79,10 +80,14 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
}
|
||||
|
||||
// Size-based styling
|
||||
|
||||
const shouldShowHandle =
|
||||
let shouldShowHandle =
|
||||
showHandles && !suppressHandle && !fromAnyOf && !isCredential;
|
||||
|
||||
// We do not want handle for output block's name field
|
||||
if (uiType === BlockUIType.OUTPUT && fieldId === "root_name") {
|
||||
shouldShowHandle = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -107,6 +112,12 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
"line-clamp-1",
|
||||
isCredential && !shouldShowHandle && "ml-3",
|
||||
size == "large" && "ml-0",
|
||||
uiType === BlockUIType.OUTPUT &&
|
||||
fieldId === "root_name" &&
|
||||
"ml-3",
|
||||
uiType === BlockUIType.INPUT && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK_MANUAL && "ml-3",
|
||||
)}
|
||||
>
|
||||
{isCredential && credentialProvider
|
||||
|
||||
Reference in New Issue
Block a user