mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
refactor(frontend): improve new builder performance and UX with position handling and store optimizations (#11397)
This PR introduces several performance and user experience improvements to the new builder, focusing on node positioning, state management optimizations, and visual enhancements. The new builder had several issues that impacted developer experience and runtime performance: - Inefficient store subscriptions causing unnecessary re-renders - No intelligent node positioning when adding blocks via clicking - useEffect dependencies causing potential stale closures - Width constraints missing on form fields affecting layout consistency ### Changes 🏗️ #### Performance Optimizations - **Store subscription optimization**: Added `useShallow` from zustand to prevent unnecessary re-renders in [NodeContainer](file:///app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContainer.tsx) and [NodeExecutionBadge](file:///app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeExecutionBadge.tsx) - **useEffect cleanup**: Split combined useEffects in [useFlow](file:///app/(platform)/build/hooks/useFlow.ts) for clearer dependencies and better performance - **Memoization**: Added `memo` to [NewControlPanel](file:///app/(platform)/build/components/NewControlPanel/NewControlPanel.tsx) to prevent unnecessary re-renders - **Callback optimization**: Wrapped `onDrop` handler in `useCallback` to prevent recreation on every render #### UX Improvements - **Smart node positioning**: Implemented `findFreePosition` algorithm in [helper.ts](file:///app/(platform)/build/components/helper.ts) that: - Automatically finds non-overlapping positions for new nodes - Tries right, left, then below existing nodes - Falls back to far-right position if no space available - **Click-to-add blocks**: Added click handlers to blocks that: - Add the block at an intelligent position - Automatically pan viewport to center the new node with smooth animation - **Visual feedback**: Added loading state with spinner icon for agent blocks during fetch - **Form field width**: Added `max-w-[340px]` constraint to prevent overflow in [FieldTemplate](file:///components/renderers/input-renderer/templates/FieldTemplate.tsx) ### 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 from scratch and execute an agent with at least 3 blocks - [x] Test adding blocks via drag-and-drop ensures no overlapping - [x] Test adding blocks via click positions them intelligently - [x] Test viewport animation when adding blocks via click - [x] Import an agent from file upload, and confirm it executes correctly - [x] Test loading spinner appears when adding agents from "My Agents" - [x] Verify performance improvements by checking React DevTools for reduced re-renders
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
import { parseAsString, useQueryStates } from "nuqs";
|
||||
import { AgentOutputs } from "./components/AgentOutputs/AgentOutputs";
|
||||
import { RunGraph } from "./components/RunGraph/RunGraph";
|
||||
import { ScheduleGraph } from "./components/ScheduleGraph/ScheduleGraph";
|
||||
import { memo } from "react";
|
||||
|
||||
export const BuilderActions = () => {
|
||||
export const BuilderActions = memo(() => {
|
||||
const [{ flowID }] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
});
|
||||
return (
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4">
|
||||
<AgentOutputs />
|
||||
<RunGraph />
|
||||
<ScheduleGraph />
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4 rounded-full bg-white p-2 px-4 shadow-lg">
|
||||
<AgentOutputs flowID={flowID} />
|
||||
<RunGraph flowID={flowID} />
|
||||
<ScheduleGraph flowID={flowID} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
BuilderActions.displayName = "BuilderActions";
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { LogOutIcon } from "lucide-react";
|
||||
import { BuilderActionButton } from "../BuilderActionButton";
|
||||
import { BookOpenIcon } from "@phosphor-icons/react";
|
||||
|
||||
export const AgentOutputs = () => {
|
||||
export const AgentOutputs = ({ flowID }: { flowID: string | null }) => {
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{/* Todo: Implement Agent Outputs */}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className={"relative min-w-0 border-none text-lg"}
|
||||
>
|
||||
<LogOutIcon className="size-6" />
|
||||
</Button>
|
||||
<BuilderActionButton disabled={!flowID}>
|
||||
<BookOpenIcon className="size-6" />
|
||||
</BuilderActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Agent Outputs</p>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ButtonProps } from "@/components/atoms/Button/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CircleNotchIcon } from "@phosphor-icons/react";
|
||||
|
||||
export const BuilderActionButton = ({
|
||||
children,
|
||||
className,
|
||||
isLoading,
|
||||
...props
|
||||
}: ButtonProps & { isLoading?: boolean }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
size={"small"}
|
||||
className={cn(
|
||||
"relative h-12 w-12 min-w-0 text-lg",
|
||||
"bg-gradient-to-br from-zinc-50 to-zinc-200",
|
||||
"border border-zinc-200",
|
||||
"shadow-[inset_0_3px_0_0_rgba(255,255,255,0.5),0_2px_4px_0_rgba(0,0,0,0.2)]",
|
||||
"dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.1),0_2px_4px_0_rgba(0,0,0,0.4)]",
|
||||
"hover:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.5),0_1px_2px_0_rgba(0,0,0,0.2)]",
|
||||
"active:shadow-[inset_0_2px_4px_0_rgba(0,0,0,0.2)]",
|
||||
"transition-all duration-150",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!isLoading ? (
|
||||
children
|
||||
) : (
|
||||
<CircleNotchIcon className="size-6 animate-spin" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlayIcon } from "lucide-react";
|
||||
import { useRunGraph } from "./useRunGraph";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { StopIcon } from "@phosphor-icons/react";
|
||||
import { PlayIcon, StopIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
||||
import {
|
||||
@@ -11,14 +9,16 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { BuilderActionButton } from "../BuilderActionButton";
|
||||
|
||||
export const RunGraph = () => {
|
||||
export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
||||
const {
|
||||
handleRunGraph,
|
||||
handleStopGraph,
|
||||
isSaving,
|
||||
openRunInputDialog,
|
||||
setOpenRunInputDialog,
|
||||
isExecutingGraph,
|
||||
isSaving,
|
||||
} = useRunGraph();
|
||||
const isGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.isGraphRunning),
|
||||
@@ -28,20 +28,21 @@ export const RunGraph = () => {
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
<BuilderActionButton
|
||||
className={cn(
|
||||
"relative min-w-0 border-none bg-gradient-to-r from-purple-500 to-pink-500 text-lg",
|
||||
isGraphRunning &&
|
||||
"border-red-500 bg-gradient-to-br from-red-400 to-red-500 shadow-[inset_0_2px_0_0_rgba(255,255,255,0.5),0_2px_4px_0_rgba(0,0,0,0.2)]",
|
||||
)}
|
||||
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
|
||||
disabled={!flowID || isExecutingGraph}
|
||||
isLoading={isExecutingGraph || isSaving}
|
||||
>
|
||||
{!isGraphRunning && !isSaving ? (
|
||||
<PlayIcon className="size-6" />
|
||||
{!isGraphRunning ? (
|
||||
<PlayIcon className="size-6 drop-shadow-sm" />
|
||||
) : (
|
||||
<StopIcon className="size-6" />
|
||||
<StopIcon className="size-6 drop-shadow-sm" />
|
||||
)}
|
||||
</Button>
|
||||
</BuilderActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isGraphRunning ? "Stop agent" : "Run agent"}
|
||||
|
||||
@@ -31,25 +31,26 @@ export const useRunGraph = () => {
|
||||
flowExecutionID: parseAsString,
|
||||
});
|
||||
|
||||
const { mutateAsync: executeGraph } = usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const { id } = response.data as GraphExecutionMeta;
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
const { mutateAsync: executeGraph, isPending: isExecutingGraph } =
|
||||
usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const { id } = response.data as GraphExecutionMeta;
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const { mutateAsync: stopGraph } = usePostV1StopGraphExecution({
|
||||
mutation: {
|
||||
@@ -72,7 +73,6 @@ export const useRunGraph = () => {
|
||||
if (hasInputs() || hasCredentials()) {
|
||||
setOpenRunInputDialog(true);
|
||||
} else {
|
||||
setIsGraphRunning(true);
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
@@ -95,6 +95,7 @@ export const useRunGraph = () => {
|
||||
handleRunGraph,
|
||||
handleStopGraph,
|
||||
isSaving,
|
||||
isExecutingGraph,
|
||||
openRunInputDialog,
|
||||
setOpenRunInputDialog,
|
||||
};
|
||||
|
||||
@@ -43,7 +43,6 @@ export const useRunInputDialog = ({
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
setIsGraphRunning(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
@@ -81,7 +80,6 @@ export const useRunInputDialog = ({
|
||||
|
||||
const handleManualRun = () => {
|
||||
setIsOpen(false);
|
||||
setIsGraphRunning(true);
|
||||
executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ClockIcon } from "@phosphor-icons/react";
|
||||
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
||||
import { useScheduleGraph } from "./useScheduleGraph";
|
||||
@@ -9,8 +8,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { CronSchedulerDialog } from "../CronSchedulerDialog/CronSchedulerDialog";
|
||||
import { BuilderActionButton } from "../BuilderActionButton";
|
||||
|
||||
export const ScheduleGraph = () => {
|
||||
export const ScheduleGraph = ({ flowID }: { flowID: string | null }) => {
|
||||
const {
|
||||
openScheduleInputDialog,
|
||||
setOpenScheduleInputDialog,
|
||||
@@ -23,14 +23,12 @@ export const ScheduleGraph = () => {
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className={"relative min-w-0 border-none text-lg"}
|
||||
<BuilderActionButton
|
||||
onClick={handleScheduleGraph}
|
||||
disabled={!flowID}
|
||||
>
|
||||
<ClockIcon className="size-6" />
|
||||
</Button>
|
||||
</BuilderActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Schedule Graph</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactFlow, Background, Controls } from "@xyflow/react";
|
||||
import { ReactFlow, Background } from "@xyflow/react";
|
||||
import NewControlPanel from "../../NewControlPanel/NewControlPanel";
|
||||
import CustomEdge from "../edges/CustomEdge";
|
||||
import { useFlow } from "./useFlow";
|
||||
@@ -13,6 +13,7 @@ import { BuilderActions } from "../../BuilderActions/BuilderActions";
|
||||
import { RunningBackground } from "./components/RunningBackground";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
import { useCopyPaste } from "./useCopyPaste";
|
||||
import { CustomControls } from "./components/CustomControl";
|
||||
|
||||
export const Flow = () => {
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
@@ -24,7 +25,8 @@ export const Flow = () => {
|
||||
const { edges, onConnect, onEdgesChange } = useCustomEdge();
|
||||
|
||||
// We use this hook to load the graph and convert them into custom nodes and edges.
|
||||
const { onDragOver, onDrop } = useFlow();
|
||||
const { onDragOver, onDrop, isFlowContentLoading, isLocked, setIsLocked } =
|
||||
useFlow();
|
||||
|
||||
// This hook is used for websocket realtime updates.
|
||||
useFlowRealtime();
|
||||
@@ -42,8 +44,6 @@ export const Flow = () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleCopyPaste]);
|
||||
|
||||
const { isFlowContentLoading } = useFlow();
|
||||
const { isGraphRunning } = useGraphStore();
|
||||
return (
|
||||
<div className="flex h-full w-full dark:bg-slate-900">
|
||||
@@ -60,12 +60,15 @@ export const Flow = () => {
|
||||
minZoom={0.1}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
nodesDraggable={!isLocked}
|
||||
nodesConnectable={!isLocked}
|
||||
elementsSelectable={!isLocked}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<CustomControls setIsLocked={setIsLocked} isLocked={isLocked} />
|
||||
<NewControlPanel />
|
||||
<BuilderActions />
|
||||
{isFlowContentLoading && <GraphLoadingBox />}
|
||||
{<GraphLoadingBox flowContentLoading={isFlowContentLoading} />}
|
||||
{isGraphRunning && <RunningBackground />}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import {
|
||||
FrameCornersIcon,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
} from "@phosphor-icons/react/dist/ssr";
|
||||
import { LockIcon, LockOpenIcon } from "lucide-react";
|
||||
import { memo } from "react";
|
||||
|
||||
export const CustomControls = memo(
|
||||
({
|
||||
setIsLocked,
|
||||
isLocked,
|
||||
}: {
|
||||
isLocked: boolean;
|
||||
setIsLocked: (isLocked: boolean) => void;
|
||||
}) => {
|
||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
|
||||
const controls = [
|
||||
{
|
||||
icon: <PlusIcon className="size-4" />,
|
||||
label: "Zoom In",
|
||||
onClick: () => zoomIn(),
|
||||
className: "h-10 w-10 border-none",
|
||||
},
|
||||
{
|
||||
icon: <MinusIcon className="size-4" />,
|
||||
label: "Zoom Out",
|
||||
onClick: () => zoomOut(),
|
||||
className: "h-10 w-10 border-none",
|
||||
},
|
||||
{
|
||||
icon: <FrameCornersIcon className="size-4" />,
|
||||
label: "Fit View",
|
||||
onClick: () => fitView({ padding: 0.2, duration: 800, maxZoom: 1 }),
|
||||
className: "h-10 w-10 border-none",
|
||||
},
|
||||
{
|
||||
icon: !isLocked ? (
|
||||
<LockOpenIcon className="size-4" />
|
||||
) : (
|
||||
<LockIcon className="size-4" />
|
||||
),
|
||||
label: "Toggle Lock",
|
||||
onClick: () => setIsLocked(!isLocked),
|
||||
className: `h-10 w-10 border-none ${isLocked ? "bg-zinc-100" : "bg-white"}`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-4 left-4 z-10 flex flex-col items-center gap-2 rounded-full bg-white px-1 py-2 shadow-lg">
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="icon"
|
||||
size={"small"}
|
||||
onClick={control.onClick}
|
||||
className={control.className}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CustomControls.displayName = "CustomControls";
|
||||
@@ -1,6 +1,28 @@
|
||||
import {
|
||||
getPostV1CreateNewGraphMutationOptions,
|
||||
getPutV1UpdateGraphVersionMutationOptions,
|
||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
|
||||
export const GraphLoadingBox = ({
|
||||
flowContentLoading,
|
||||
}: {
|
||||
flowContentLoading: boolean;
|
||||
}) => {
|
||||
const isCreating = useIsMutating({
|
||||
mutationKey: getPostV1CreateNewGraphMutationOptions().mutationKey,
|
||||
});
|
||||
const isUpdating = useIsMutating({
|
||||
mutationKey: getPutV1UpdateGraphVersionMutationOptions().mutationKey,
|
||||
});
|
||||
|
||||
const isSaving = !!(isCreating || isUpdating);
|
||||
|
||||
if (!flowContentLoading && !isSaving) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const GraphLoadingBox = () => {
|
||||
return (
|
||||
<div className="absolute left-[50%] top-[50%] z-[99] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="flex flex-col items-center gap-4 rounded-xlarge border border-gray-200 bg-white p-8 shadow-lg dark:border-gray-700 dark:bg-slate-800">
|
||||
@@ -8,8 +30,15 @@ export const GraphLoadingBox = () => {
|
||||
<div className="absolute inset-0 animate-spin rounded-full border-4 border-violet-200 border-t-violet-500 dark:border-gray-700 dark:border-t-blue-400"></div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Text variant="h4">Loading Flow</Text>
|
||||
<Text variant="small">Please wait while we load your graph...</Text>
|
||||
{isSaving && <Text variant="h4">Saving Graph</Text>}
|
||||
{flowContentLoading && <Text variant="h4">Loading Flow</Text>}
|
||||
|
||||
{isSaving && (
|
||||
<Text variant="small">Please wait while we save your graph...</Text>
|
||||
)}
|
||||
{flowContentLoading && (
|
||||
<Text variant="small">Please wait while we load your graph...</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,154 +2,53 @@ export const RunningBackground = () => {
|
||||
return (
|
||||
<div className="absolute inset-0 h-full w-full">
|
||||
<style jsx>{`
|
||||
@keyframes rotateGradient {
|
||||
0% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#bc82f3 17%,
|
||||
#f5b9ea 24%,
|
||||
#8d99ff 35%,
|
||||
#aa6eee 58%,
|
||||
#ff6778 70%,
|
||||
#ffba71 81%,
|
||||
#c686ff 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
14.28% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#c686ff 17%,
|
||||
#bc82f3 24%,
|
||||
#f5b9ea 35%,
|
||||
#8d99ff 58%,
|
||||
#aa6eee 70%,
|
||||
#ff6778 81%,
|
||||
#ffba71 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
28.56% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#ffba71 17%,
|
||||
#c686ff 24%,
|
||||
#bc82f3 35%,
|
||||
#f5b9ea 58%,
|
||||
#8d99ff 70%,
|
||||
#aa6eee 81%,
|
||||
#ff6778 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
42.84% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#ff6778 17%,
|
||||
#ffba71 24%,
|
||||
#c686ff 35%,
|
||||
#bc82f3 58%,
|
||||
#f5b9ea 70%,
|
||||
#8d99ff 81%,
|
||||
#aa6eee 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
57.12% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#aa6eee 17%,
|
||||
#ff6778 24%,
|
||||
#ffba71 35%,
|
||||
#c686ff 58%,
|
||||
#bc82f3 70%,
|
||||
#f5b9ea 81%,
|
||||
#8d99ff 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
71.4% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#8d99ff 17%,
|
||||
#aa6eee 24%,
|
||||
#ff6778 35%,
|
||||
#ffba71 58%,
|
||||
#c686ff 70%,
|
||||
#bc82f3 81%,
|
||||
#f5b9ea 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
85.68% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#f5b9ea 17%,
|
||||
#8d99ff 24%,
|
||||
#aa6eee 35%,
|
||||
#ff6778 58%,
|
||||
#ffba71 70%,
|
||||
#c686ff 81%,
|
||||
#bc82f3 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#bc82f3 17%,
|
||||
#f5b9ea 24%,
|
||||
#8d99ff 35%,
|
||||
#aa6eee 58%,
|
||||
#ff6778 70%,
|
||||
#ffba71 81%,
|
||||
#c686ff 92%
|
||||
)
|
||||
1;
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.animate-gradient {
|
||||
animation: rotateGradient 8s linear infinite;
|
||||
.animate-pulse-border {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-xl"
|
||||
className="animate-pulse-border absolute inset-0 bg-transparent blur-xl"
|
||||
style={{
|
||||
borderWidth: "15px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
borderImage: "linear-gradient(to right, #BC82F3, #BC82F3) 1",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-lg"
|
||||
className="animate-pulse-border absolute inset-0 bg-transparent blur-lg"
|
||||
style={{
|
||||
borderWidth: "10px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
borderImage: "linear-gradient(to right, #BC82F3, #BC82F3) 1",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-md"
|
||||
className="animate-pulse-border absolute inset-0 bg-transparent blur-md"
|
||||
style={{
|
||||
borderWidth: "6px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
borderImage: "linear-gradient(to right, #BC82F3, #BC82F3) 1",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-sm"
|
||||
className="animate-pulse-border absolute inset-0 bg-transparent blur-sm"
|
||||
style={{
|
||||
borderWidth: "6px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
borderImage: "linear-gradient(to right, #BC82F3, #BC82F3) 1",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import {
|
||||
useGetV1GetExecutionDetails,
|
||||
@@ -18,6 +18,7 @@ import { useReactFlow } from "@xyflow/react";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
|
||||
export const useFlow = () => {
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const addNodes = useNodeStore(useShallow((state) => state.addNodes));
|
||||
const addLinks = useEdgeStore(useShallow((state) => state.addLinks));
|
||||
const updateNodeStatus = useNodeStore(
|
||||
@@ -69,7 +70,9 @@ export const useFlow = () => {
|
||||
);
|
||||
|
||||
const nodes = graph?.nodes;
|
||||
const blockIds = nodes?.map((node) => node.block_id);
|
||||
const blockIds = nodes
|
||||
? Array.from(new Set(nodes.map((node) => node.block_id)))
|
||||
: undefined;
|
||||
|
||||
const { data: blocks, isLoading: isBlocksLoading } =
|
||||
useGetV2GetSpecificBlocks(
|
||||
@@ -95,34 +98,43 @@ export const useFlow = () => {
|
||||
});
|
||||
}, [nodes, blocks]);
|
||||
|
||||
// load graph schemas
|
||||
useEffect(() => {
|
||||
// load graph schemas
|
||||
if (graph) {
|
||||
setGraphSchemas(
|
||||
graph.input_schema as Record<string, any> | null,
|
||||
graph.credentials_input_schema as Record<string, any> | null,
|
||||
);
|
||||
}
|
||||
}, [graph]);
|
||||
|
||||
// adding nodes
|
||||
// adding nodes
|
||||
useEffect(() => {
|
||||
if (customNodes.length > 0) {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
addNodes(customNodes);
|
||||
}
|
||||
}, [customNodes, addNodes]);
|
||||
|
||||
// adding links
|
||||
// adding links
|
||||
useEffect(() => {
|
||||
if (graph?.links) {
|
||||
useEdgeStore.getState().setEdges([]);
|
||||
addLinks(graph.links);
|
||||
}
|
||||
}, [graph?.links, addLinks]);
|
||||
|
||||
// update graph running status
|
||||
// update graph running status
|
||||
useEffect(() => {
|
||||
const isRunning =
|
||||
executionDetails?.status === AgentExecutionStatus.RUNNING ||
|
||||
executionDetails?.status === AgentExecutionStatus.QUEUED;
|
||||
setIsGraphRunning(isRunning);
|
||||
|
||||
// update node execution status in nodes
|
||||
setIsGraphRunning(isRunning);
|
||||
}, [executionDetails?.status]);
|
||||
|
||||
// update node execution status in nodes
|
||||
useEffect(() => {
|
||||
if (
|
||||
executionDetails &&
|
||||
"node_executions" in executionDetails &&
|
||||
@@ -132,8 +144,10 @@ export const useFlow = () => {
|
||||
updateNodeStatus(nodeExecution.node_id, nodeExecution.status);
|
||||
});
|
||||
}
|
||||
}, [executionDetails, updateNodeStatus]);
|
||||
|
||||
// update node execution results in nodes, also update edge beads
|
||||
// update node execution results in nodes, also update edge beads
|
||||
useEffect(() => {
|
||||
if (
|
||||
executionDetails &&
|
||||
"node_executions" in executionDetails &&
|
||||
@@ -144,7 +158,7 @@ export const useFlow = () => {
|
||||
updateEdgeBeads(nodeExecution.node_id, nodeExecution);
|
||||
});
|
||||
}
|
||||
}, [customNodes, addNodes, graph?.links, executionDetails, updateNodeStatus]);
|
||||
}, [executionDetails, updateNodeExecutionResult, updateEdgeBeads]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -162,30 +176,37 @@ export const useFlow = () => {
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}, []);
|
||||
|
||||
const onDrop = async (event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
const blockDataString = event.dataTransfer.getData("application/reactflow");
|
||||
if (!blockDataString) return;
|
||||
const onDrop = useCallback(
|
||||
async (event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
const blockDataString = event.dataTransfer.getData(
|
||||
"application/reactflow",
|
||||
);
|
||||
if (!blockDataString) return;
|
||||
|
||||
try {
|
||||
const blockData = JSON.parse(blockDataString) as BlockInfo;
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
addBlock(blockData, position);
|
||||
try {
|
||||
const blockData = JSON.parse(blockDataString) as BlockInfo;
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
addBlock(blockData, {}, position);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
setBlockMenuOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to drop block:", error);
|
||||
setBlockMenuOpen(true);
|
||||
}
|
||||
};
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
setBlockMenuOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to drop block:", error);
|
||||
setBlockMenuOpen(true);
|
||||
}
|
||||
},
|
||||
[screenToFlowPosition, addBlock, setBlockMenuOpen],
|
||||
);
|
||||
|
||||
return {
|
||||
isFlowContentLoading: isGraphLoading || isBlocksLoading,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
isLocked,
|
||||
setIsLocked,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cn } from "@/lib/utils";
|
||||
import { nodeStyleBasedOnStatus } from "../helpers";
|
||||
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const NodeContainer = ({
|
||||
children,
|
||||
@@ -12,7 +13,9 @@ export const NodeContainer = ({
|
||||
nodeId: string;
|
||||
selected: boolean;
|
||||
}) => {
|
||||
const status = useNodeStore((state) => state.getNodeStatus(nodeId));
|
||||
const status = useNodeStore(
|
||||
useShallow((state) => state.getNodeStatus(nodeId)),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
|
||||
import { Badge } from "@/components/__legacy__/ui/badge";
|
||||
import { LoadingSpinner } from "@/components/__legacy__/ui/loading";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
const statusStyles: Record<AgentExecutionStatus, string> = {
|
||||
INCOMPLETE: "text-slate-700 border-slate-400",
|
||||
@@ -14,7 +15,9 @@ const statusStyles: Record<AgentExecutionStatus, string> = {
|
||||
};
|
||||
|
||||
export const NodeExecutionBadge = ({ nodeId }: { nodeId: string }) => {
|
||||
const status = useNodeStore((state) => state.getNodeStatus(nodeId));
|
||||
const status = useNodeStore(
|
||||
useShallow((state) => state.getNodeStatus(nodeId)),
|
||||
);
|
||||
if (!status) return null;
|
||||
return (
|
||||
<div className="flex items-center justify-end rounded-b-xl py-2 pr-4">
|
||||
|
||||
@@ -7,6 +7,8 @@ import { PlusIcon } from "@phosphor-icons/react";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { blockDragPreviewStyle } from "./style";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@@ -29,6 +31,23 @@ export const Block: BlockComponent = ({
|
||||
const setBlockMenuOpen = useControlPanelStore(
|
||||
(state) => state.setBlockMenuOpen,
|
||||
);
|
||||
const { setViewport } = useReactFlow();
|
||||
const { addBlock } = useNodeStore();
|
||||
|
||||
const handleClick = () => {
|
||||
const customNode = addBlock(blockData);
|
||||
setTimeout(() => {
|
||||
setViewport(
|
||||
{
|
||||
x: -customNode.position.x * 0.8 + window.innerWidth / 2,
|
||||
y: -customNode.position.y * 0.8 + (window.innerHeight - 400) / 2,
|
||||
zoom: 0.8,
|
||||
},
|
||||
{ duration: 500 },
|
||||
);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleDragStart = (e: React.DragEvent<HTMLButtonElement>) => {
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.dataTransfer.setData("application/reactflow", JSON.stringify(blockData));
|
||||
@@ -55,6 +74,7 @@ export const Block: BlockComponent = ({
|
||||
className,
|
||||
)}
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
<div className="flex flex-1 flex-col items-start gap-0.5">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { Block } from "../Block";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
|
||||
interface BlocksListProps {
|
||||
@@ -13,7 +12,6 @@ export const BlocksList: React.FC<BlocksListProps> = ({
|
||||
blocks,
|
||||
loading = false,
|
||||
}) => {
|
||||
const { addBlock } = useNodeStore();
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
@@ -28,7 +26,6 @@ export const BlocksList: React.FC<BlocksListProps> = ({
|
||||
key={block.id}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
onClick={() => addBlock(block)}
|
||||
blockData={block}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NoSearchResult } from "../NoSearchResult";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
|
||||
export const BlockMenuSearch = () => {
|
||||
const {
|
||||
@@ -22,7 +21,6 @@ export const BlockMenuSearch = () => {
|
||||
searchLoading,
|
||||
} = useBlockMenuSearch();
|
||||
const { searchQuery } = useBlockMenuStore();
|
||||
const addBlock = useNodeStore((state) => state.addBlock);
|
||||
|
||||
if (searchLoading) {
|
||||
return (
|
||||
@@ -75,7 +73,6 @@ export const BlockMenuSearch = () => {
|
||||
title={data.name}
|
||||
highlightedText={searchQuery}
|
||||
description={data.description}
|
||||
onClick={() => addBlock(data)}
|
||||
blockData={data}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { useIntegrationBlocks } from "./useIntegrationBlocks";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
export const IntegrationBlocks = () => {
|
||||
@@ -21,7 +20,6 @@ export const IntegrationBlocks = () => {
|
||||
error,
|
||||
refetch,
|
||||
} = useIntegrationBlocks();
|
||||
const addBlock = useNodeStore((state) => state.addBlock);
|
||||
|
||||
if (blocksLoading) {
|
||||
return (
|
||||
@@ -93,8 +91,8 @@ export const IntegrationBlocks = () => {
|
||||
key={block.id}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
blockData={block}
|
||||
icon_url={`/integrations/${integration}.png`}
|
||||
onClick={() => addBlock(block)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,18 @@ import Image from "next/image";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { highlightText } from "./helpers";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { blockDragPreviewStyle } from "./style";
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon_url?: string;
|
||||
highlightedText?: string;
|
||||
blockData: BlockInfo;
|
||||
}
|
||||
|
||||
interface IntegrationBlockComponent extends React.FC<Props> {
|
||||
@@ -23,16 +29,57 @@ export const IntegrationBlock: IntegrationBlockComponent = ({
|
||||
description,
|
||||
className,
|
||||
highlightedText,
|
||||
blockData,
|
||||
...rest
|
||||
}) => {
|
||||
const setBlockMenuOpen = useControlPanelStore(
|
||||
(state) => state.setBlockMenuOpen,
|
||||
);
|
||||
const { setViewport } = useReactFlow();
|
||||
const { addBlock } = useNodeStore();
|
||||
|
||||
const handleClick = () => {
|
||||
const customNode = addBlock(blockData);
|
||||
setTimeout(() => {
|
||||
setViewport(
|
||||
{
|
||||
x: -customNode.position.x * 0.8 + window.innerWidth / 2,
|
||||
y: -customNode.position.y * 0.8 + (window.innerHeight - 400) / 2,
|
||||
zoom: 0.8,
|
||||
},
|
||||
{ duration: 500 },
|
||||
);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleDragStart = (e: React.DragEvent<HTMLButtonElement>) => {
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.dataTransfer.setData("application/reactflow", JSON.stringify(blockData));
|
||||
|
||||
setBlockMenuOpen(false);
|
||||
|
||||
// preview when user drags it
|
||||
const dragPreview = document.createElement("div");
|
||||
dragPreview.style.cssText = blockDragPreviewStyle;
|
||||
dragPreview.textContent = beautifyString(title || "");
|
||||
|
||||
document.body.appendChild(dragPreview);
|
||||
e.dataTransfer.setDragImage(dragPreview, 0, 0);
|
||||
|
||||
setTimeout(() => document.body.removeChild(dragPreview), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
draggable={true}
|
||||
variant={"ghost"}
|
||||
className={cn(
|
||||
"group flex h-16 w-full min-w-[7.5rem] items-center justify-start gap-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
|
||||
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
<div className="relative h-[2.625rem] w-[2.625rem] rounded-[0.5rem] bg-white">
|
||||
|
||||
@@ -16,6 +16,9 @@ export const MyAgentsContent = () => {
|
||||
error,
|
||||
status,
|
||||
refetch,
|
||||
handleAddBlock,
|
||||
isGettingAgentDetails,
|
||||
selectedAgentId,
|
||||
} = useMyAgentsContent();
|
||||
|
||||
if (agentLoading) {
|
||||
@@ -59,7 +62,9 @@ export const MyAgentsContent = () => {
|
||||
title={agent.name}
|
||||
edited_time={agent.updated_at}
|
||||
version={agent.graph_version}
|
||||
isLoading={isGettingAgentDetails && selectedAgentId === agent.id}
|
||||
image_url={agent.image_url}
|
||||
onClick={() => handleAddBlock(agent)}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import {
|
||||
getV2GetLibraryAgent,
|
||||
useGetV2ListLibraryAgentsInfinite,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
import { useState } from "react";
|
||||
import { convertLibraryAgentIntoCustomNode } from "../helpers";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
|
||||
export const useMyAgentsContent = () => {
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [isGettingAgentDetails, setIsGettingAgentDetails] = useState(false);
|
||||
const addBlock = useNodeStore(useShallow((state) => state.addBlock));
|
||||
const { setViewport } = useReactFlow();
|
||||
// This endpoints is not giving info about inputSchema and outputSchema
|
||||
// Will create new endpoint for this
|
||||
const {
|
||||
data: agents,
|
||||
fetchNextPage,
|
||||
@@ -38,6 +53,43 @@ export const useMyAgentsContent = () => {
|
||||
|
||||
const status = agents?.pages[0]?.status;
|
||||
|
||||
const handleAddBlock = async (agent: LibraryAgent) => {
|
||||
setSelectedAgentId(agent.id);
|
||||
setIsGettingAgentDetails(true);
|
||||
|
||||
try {
|
||||
const response = await getV2GetLibraryAgent(agent.id);
|
||||
|
||||
if (!response.data) {
|
||||
console.error("Failed to get agent details", selectedAgentId, agent.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const { input_schema, output_schema } = response.data as LibraryAgent;
|
||||
const { block, hardcodedValues } = convertLibraryAgentIntoCustomNode(
|
||||
agent,
|
||||
input_schema,
|
||||
output_schema,
|
||||
);
|
||||
const customNode = addBlock(block, hardcodedValues);
|
||||
setTimeout(() => {
|
||||
setViewport(
|
||||
{
|
||||
x: -customNode.position.x * 0.8 + window.innerWidth / 2,
|
||||
y: -customNode.position.y * 0.8 + (window.innerHeight - 400) / 2,
|
||||
zoom: 0.8,
|
||||
},
|
||||
{ duration: 500 },
|
||||
);
|
||||
}, 50);
|
||||
} catch (error) {
|
||||
console.error("Error adding block:", error);
|
||||
} finally {
|
||||
setSelectedAgentId(null);
|
||||
setIsGettingAgentDetails(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
allAgents,
|
||||
agentLoading,
|
||||
@@ -48,5 +100,8 @@ export const useMyAgentsContent = () => {
|
||||
refetch,
|
||||
error,
|
||||
status,
|
||||
handleAddBlock,
|
||||
isGettingAgentDetails,
|
||||
selectedAgentId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,14 +4,12 @@ import { Block } from "../Block";
|
||||
import { useSuggestionContent } from "./useSuggestionContent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { DefaultStateType } from "../types";
|
||||
|
||||
export const SuggestionContent = () => {
|
||||
const { setIntegration, setDefaultState } = useBlockMenuStore();
|
||||
const { data, isLoading, isError, error, refetch } = useSuggestionContent();
|
||||
const addBlock = useNodeStore((state) => state.addBlock);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
@@ -76,7 +74,6 @@ export const SuggestionContent = () => {
|
||||
key={`block-${index}`}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
onClick={() => addBlock(block)}
|
||||
blockData={block}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Button } from "@/components/__legacy__/ui/button";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { highlightText } from "./helpers";
|
||||
import { formatTimeAgo } from "@/lib/utils/time";
|
||||
import { CircleNotchIcon } from "@phosphor-icons/react";
|
||||
import { PlusIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
isLoading?: boolean;
|
||||
title?: string;
|
||||
edited_time?: Date;
|
||||
version?: number;
|
||||
@@ -20,6 +22,7 @@ interface UGCAgentBlockComponent extends React.FC<Props> {
|
||||
}
|
||||
|
||||
export const UGCAgentBlock: UGCAgentBlockComponent = ({
|
||||
isLoading,
|
||||
title,
|
||||
image_url,
|
||||
edited_time = new Date(),
|
||||
@@ -85,7 +88,11 @@ export const UGCAgentBlock: UGCAgentBlockComponent = ({
|
||||
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
|
||||
)}
|
||||
>
|
||||
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
|
||||
{isLoading ? (
|
||||
<CircleNotchIcon className="h-5 w-5 animate-spin text-zinc-50" />
|
||||
) : (
|
||||
<PlusIcon className="h-5 w-5 text-zinc-50" strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { BlockUIType } from "../../types";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { BlockCategory } from "../../helper";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { SpecialBlockID } from "@/lib/autogpt-server-api";
|
||||
|
||||
export const highlightText = (
|
||||
text: string | undefined,
|
||||
highlight: string | undefined,
|
||||
@@ -20,3 +27,37 @@ export const highlightText = (
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const convertLibraryAgentIntoCustomNode = (
|
||||
agent: LibraryAgent,
|
||||
inputSchema: RJSFSchema = {} as RJSFSchema,
|
||||
outputSchema: RJSFSchema = {} as RJSFSchema,
|
||||
) => {
|
||||
const block: BlockInfo = {
|
||||
id: SpecialBlockID.AGENT,
|
||||
name: agent.name,
|
||||
description:
|
||||
`Ver.${agent.graph_version}` +
|
||||
(agent.description ? ` | ${agent.description}` : ""),
|
||||
categories: [{ category: BlockCategory.AGENT, description: "" }],
|
||||
inputSchema: inputSchema,
|
||||
outputSchema: outputSchema,
|
||||
staticOutput: false,
|
||||
uiType: BlockUIType.AGENT,
|
||||
costs: [],
|
||||
contributors: [],
|
||||
};
|
||||
|
||||
const hardcodedValues: Record<string, any> = {
|
||||
graph_id: agent.graph_id,
|
||||
graph_version: agent.graph_version,
|
||||
input_schema: inputSchema,
|
||||
output_schema: outputSchema,
|
||||
agent_name: agent.name,
|
||||
};
|
||||
|
||||
return {
|
||||
block,
|
||||
hardcodedValues,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { memo } from "react";
|
||||
import { BlockMenu } from "./NewBlockMenu/BlockMenu/BlockMenu";
|
||||
import { useNewControlPanel } from "./useNewControlPanel";
|
||||
// import { NewSaveControl } from "../SaveControl/NewSaveControl";
|
||||
import { GraphExecutionID } from "@/lib/autogpt-server-api";
|
||||
// import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { ArrowUUpLeftIcon, ArrowUUpRightIcon } from "@phosphor-icons/react";
|
||||
// import { GraphSearchMenu } from "../GraphMenu/GraphMenu";
|
||||
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";
|
||||
@@ -31,56 +29,39 @@ export type NewControlPanelProps = {
|
||||
onNodeSelect?: (nodeId: string) => void;
|
||||
onNodeHover?: (nodeId: string) => void;
|
||||
};
|
||||
export const NewControlPanel = ({
|
||||
flowExecutionID: _flowExecutionID,
|
||||
visualizeBeads: _visualizeBeads,
|
||||
pinSavePopover: _pinSavePopover,
|
||||
pinBlocksPopover: _pinBlocksPopover,
|
||||
nodes: _nodes,
|
||||
onNodeSelect: _onNodeSelect,
|
||||
onNodeHover: _onNodeHover,
|
||||
}: NewControlPanelProps) => {
|
||||
const _isGraphSearchEnabled = useGetFlag(Flag.GRAPH_SEARCH);
|
||||
export const NewControlPanel = memo(
|
||||
({
|
||||
flowExecutionID: _flowExecutionID,
|
||||
visualizeBeads: _visualizeBeads,
|
||||
pinSavePopover: _pinSavePopover,
|
||||
pinBlocksPopover: _pinBlocksPopover,
|
||||
nodes: _nodes,
|
||||
onNodeSelect: _onNodeSelect,
|
||||
onNodeHover: _onNodeHover,
|
||||
}: NewControlPanelProps) => {
|
||||
const _isGraphSearchEnabled = useGetFlag(Flag.GRAPH_SEARCH);
|
||||
|
||||
const {
|
||||
// agentDescription,
|
||||
// setAgentDescription,
|
||||
// saveAgent,
|
||||
// agentName,
|
||||
// setAgentName,
|
||||
// savedAgent,
|
||||
// isSaving,
|
||||
// isRunning,
|
||||
// isStopping,
|
||||
} = useNewControlPanel({});
|
||||
const {
|
||||
// agentDescription,
|
||||
// setAgentDescription,
|
||||
// saveAgent,
|
||||
// agentName,
|
||||
// setAgentName,
|
||||
// savedAgent,
|
||||
// isSaving,
|
||||
// isRunning,
|
||||
// isStopping,
|
||||
} = useNewControlPanel({});
|
||||
|
||||
const _controls: Control[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "Undo",
|
||||
icon: <ArrowUUpLeftIcon size={20} weight="bold" />,
|
||||
onClick: history.undo,
|
||||
disabled: !history.canUndo(),
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
icon: <ArrowUUpRightIcon size={20} weight="bold" />,
|
||||
onClick: history.redo,
|
||||
disabled: !history.canRedo(),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"absolute left-4 top-10 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
|
||||
<BlockMenu />
|
||||
{/* <Separator className="text-[#E1E1E1]" />
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"absolute left-4 top-10 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
|
||||
<BlockMenu />
|
||||
{/* <Separator className="text-[#E1E1E1]" />
|
||||
{isGraphSearchEnabled && (
|
||||
<>
|
||||
<GraphSearchMenu
|
||||
@@ -105,13 +86,16 @@ export const NewControlPanel = ({
|
||||
{control.icon}
|
||||
</ControlPanelButton>
|
||||
))} */}
|
||||
<Separator className="text-[#E1E1E1]" />
|
||||
<NewSaveControl />
|
||||
<Separator className="text-[#E1E1E1]" />
|
||||
<UndoRedoButtons />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
<Separator className="text-[#E1E1E1]" />
|
||||
<NewSaveControl />
|
||||
<Separator className="text-[#E1E1E1]" />
|
||||
<UndoRedoButtons />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default NewControlPanel;
|
||||
|
||||
NewControlPanel.displayName = "NewControlPanel";
|
||||
|
||||
@@ -42,9 +42,10 @@ export const graphsEquivalent = (
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
nodes: sortNodes(current.nodes ?? []).map(({ id: _, ...rest }) => rest),
|
||||
links: sortLinks(current.links ?? []).map(
|
||||
({ source_id: _, sink_id: __, ...rest }) => rest,
|
||||
),
|
||||
links: sortLinks(current.links ?? []).map((v) => ({
|
||||
sink_name: v.sink_name,
|
||||
source_name: v.source_name,
|
||||
})),
|
||||
};
|
||||
|
||||
return deepEquals(_saved, _current);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { NodeModel } from "@/app/api/__generated__/models/nodeModel";
|
||||
import { NodeModelMetadata } from "@/app/api/__generated__/models/nodeModelMetadata";
|
||||
import { Link } from "@/app/api/__generated__/models/link";
|
||||
import { CustomEdge } from "./FlowEditor/edges/CustomEdge";
|
||||
import { XYPosition } from "@xyflow/react";
|
||||
|
||||
export const convertBlockInfoIntoCustomNodeData = (
|
||||
block: BlockInfo,
|
||||
@@ -115,3 +116,107 @@ export const isCostFilterMatch = (
|
||||
)
|
||||
: costFilter === inputValues;
|
||||
};
|
||||
|
||||
// ----- Position related helpers -----
|
||||
|
||||
export interface NodeDimensions {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function rectanglesOverlap(
|
||||
rect1: NodeDimensions,
|
||||
rect2: NodeDimensions,
|
||||
): boolean {
|
||||
const x1 = rect1.x,
|
||||
y1 = rect1.y,
|
||||
w1 = rect1.width,
|
||||
h1 = rect1.height;
|
||||
const x2 = rect2.x,
|
||||
y2 = rect2.y,
|
||||
w2 = rect2.width,
|
||||
h2 = rect2.height;
|
||||
|
||||
return !(x1 + w1 <= x2 || x1 >= x2 + w2 || y1 + h1 <= y2 || y1 >= y2 + h2);
|
||||
}
|
||||
|
||||
export function findFreePosition(
|
||||
existingNodes: Array<{
|
||||
position: XYPosition;
|
||||
measured?: { width: number; height: number };
|
||||
}>,
|
||||
newNodeWidth: number = 500,
|
||||
margin: number = 60,
|
||||
): XYPosition {
|
||||
if (existingNodes.length === 0) {
|
||||
return { x: 100, y: 100 }; // Default starting position
|
||||
}
|
||||
|
||||
// Start from the most recently added node
|
||||
for (let i = existingNodes.length - 1; i >= 0; i--) {
|
||||
const lastNode = existingNodes[i];
|
||||
const lastNodeWidth = lastNode.measured?.width ?? 500;
|
||||
const lastNodeHeight = lastNode.measured?.height ?? 400;
|
||||
|
||||
// Try right
|
||||
const candidate = {
|
||||
x: lastNode.position.x + lastNodeWidth + margin,
|
||||
y: lastNode.position.y,
|
||||
width: newNodeWidth,
|
||||
height: 400, // Estimated height
|
||||
};
|
||||
|
||||
if (
|
||||
!existingNodes.some((n) =>
|
||||
rectanglesOverlap(candidate, {
|
||||
x: n.position.x,
|
||||
y: n.position.y,
|
||||
width: n.measured?.width ?? 500,
|
||||
height: n.measured?.height ?? 400,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
return { x: candidate.x, y: candidate.y };
|
||||
}
|
||||
|
||||
// Try left
|
||||
candidate.x = lastNode.position.x - newNodeWidth - margin;
|
||||
if (
|
||||
!existingNodes.some((n) =>
|
||||
rectanglesOverlap(candidate, {
|
||||
x: n.position.x,
|
||||
y: n.position.y,
|
||||
width: n.measured?.width ?? 500,
|
||||
height: n.measured?.height ?? 400,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
return { x: candidate.x, y: candidate.y };
|
||||
}
|
||||
|
||||
// Try below
|
||||
candidate.x = lastNode.position.x;
|
||||
candidate.y = lastNode.position.y + lastNodeHeight + margin;
|
||||
if (
|
||||
!existingNodes.some((n) =>
|
||||
rectanglesOverlap(candidate, {
|
||||
x: n.position.x,
|
||||
y: n.position.y,
|
||||
width: n.measured?.width ?? 500,
|
||||
height: n.measured?.height ?? 400,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
return { x: candidate.x, y: candidate.y };
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: place it far to the right
|
||||
const lastNode = existingNodes[existingNodes.length - 1];
|
||||
return {
|
||||
x: lastNode.position.x + 600,
|
||||
y: lastNode.position.y,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import {
|
||||
getGetV1GetSpecificGraphQueryKey,
|
||||
useGetV1GetSpecificGraph,
|
||||
usePostV1CreateNewGraph,
|
||||
usePutV1UpdateGraphVersion,
|
||||
@@ -15,6 +13,8 @@ import { Graph } from "@/app/api/__generated__/models/graph";
|
||||
import { useNodeStore } from "../stores/nodeStore";
|
||||
import { useEdgeStore } from "../stores/edgeStore";
|
||||
import { graphsEquivalent } from "../components/NewControlPanel/NewSaveControl/helpers";
|
||||
import { useGraphStore } from "../stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export type SaveGraphOptions = {
|
||||
showToast?: boolean;
|
||||
@@ -28,13 +28,16 @@ export const useSaveGraph = ({
|
||||
onError,
|
||||
}: SaveGraphOptions) => {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
});
|
||||
|
||||
const setGraphSchemas = useGraphStore(
|
||||
useShallow((state) => state.setGraphSchemas),
|
||||
);
|
||||
|
||||
const { data: graph } = useGetV1GetSpecificGraph(
|
||||
flowID ?? "",
|
||||
flowVersion !== null ? { version: flowVersion } : {},
|
||||
@@ -55,9 +58,6 @@ export const useSaveGraph = ({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
queryClient.refetchQueries({
|
||||
queryKey: getGetV1GetSpecificGraphQueryKey(data.id),
|
||||
});
|
||||
onSuccess?.(data);
|
||||
if (showToast) {
|
||||
toast({
|
||||
@@ -88,9 +88,6 @@ export const useSaveGraph = ({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
queryClient.refetchQueries({
|
||||
queryKey: getGetV1GetSpecificGraphQueryKey(data.id),
|
||||
});
|
||||
onSuccess?.(data);
|
||||
if (showToast) {
|
||||
toast({
|
||||
@@ -140,7 +137,12 @@ export const useSaveGraph = ({
|
||||
return;
|
||||
}
|
||||
|
||||
await updateGraph({ graphId: graph.id, data: data });
|
||||
const response = await updateGraph({ graphId: graph.id, data: data });
|
||||
const graphData = response.data as GraphModel;
|
||||
setGraphSchemas(
|
||||
graphData.input_schema,
|
||||
graphData.credentials_input_schema,
|
||||
);
|
||||
} else {
|
||||
const data: Graph = {
|
||||
name: values?.name || `New Agent ${new Date().toISOString()}`,
|
||||
@@ -149,7 +151,12 @@ export const useSaveGraph = ({
|
||||
links: graphLinks,
|
||||
};
|
||||
|
||||
await createNewGraph({ data: { graph: data } });
|
||||
const response = await createNewGraph({ data: { graph: data } });
|
||||
const graphData = response.data as GraphModel;
|
||||
setGraphSchemas(
|
||||
graphData.input_schema,
|
||||
graphData.credentials_input_schema,
|
||||
);
|
||||
}
|
||||
},
|
||||
[graph, toast, createNewGraph, updateGraph],
|
||||
|
||||
@@ -2,7 +2,10 @@ import { create } from "zustand";
|
||||
import { NodeChange, XYPosition, applyNodeChanges } from "@xyflow/react";
|
||||
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode/CustomNode";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { convertBlockInfoIntoCustomNodeData } from "../components/helper";
|
||||
import {
|
||||
convertBlockInfoIntoCustomNodeData,
|
||||
findFreePosition,
|
||||
} from "../components/helper";
|
||||
import { Node } from "@/app/api/__generated__/models/node";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
@@ -17,7 +20,11 @@ type NodeStore = {
|
||||
setNodes: (nodes: CustomNode[]) => void;
|
||||
onNodesChange: (changes: NodeChange<CustomNode>[]) => void;
|
||||
addNode: (node: CustomNode) => void;
|
||||
addBlock: (block: BlockInfo, position?: XYPosition) => void;
|
||||
addBlock: (
|
||||
block: BlockInfo,
|
||||
hardcodedValues?: Record<string, any>,
|
||||
position?: XYPosition,
|
||||
) => CustomNode;
|
||||
incrementNodeCounter: () => void;
|
||||
updateNodeData: (nodeId: string, data: Partial<CustomNode["data"]>) => void;
|
||||
toggleAdvanced: (nodeId: string) => void;
|
||||
@@ -36,7 +43,6 @@ type NodeStore = {
|
||||
result: NodeExecutionResult,
|
||||
) => void;
|
||||
getNodeExecutionResult: (nodeId: string) => NodeExecutionResult | undefined;
|
||||
|
||||
getNodeBlockUIType: (nodeId: string) => BlockUIType;
|
||||
};
|
||||
|
||||
@@ -74,19 +80,42 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
nodes: [...state.nodes, node],
|
||||
}));
|
||||
},
|
||||
addBlock: (block: BlockInfo, position?: XYPosition) => {
|
||||
const customNodeData = convertBlockInfoIntoCustomNodeData(block);
|
||||
addBlock: (
|
||||
block: BlockInfo,
|
||||
hardcodedValues?: Record<string, any>,
|
||||
position?: XYPosition,
|
||||
) => {
|
||||
const customNodeData = convertBlockInfoIntoCustomNodeData(
|
||||
block,
|
||||
hardcodedValues,
|
||||
);
|
||||
get().incrementNodeCounter();
|
||||
const nodeNumber = get().nodeCounter;
|
||||
|
||||
const nodePosition =
|
||||
position ||
|
||||
findFreePosition(
|
||||
get().nodes.map((node) => ({
|
||||
position: node.position,
|
||||
measured: {
|
||||
width: node.data.uiType === BlockUIType.NOTE ? 300 : 500,
|
||||
height: 400,
|
||||
},
|
||||
})),
|
||||
block.uiType === BlockUIType.NOTE ? 300 : 400,
|
||||
30,
|
||||
);
|
||||
|
||||
const customNode: CustomNode = {
|
||||
id: nodeNumber.toString(),
|
||||
data: customNodeData,
|
||||
type: "custom",
|
||||
position: position || ({ x: 0, y: 0 } as XYPosition),
|
||||
position: nodePosition,
|
||||
};
|
||||
set((state) => ({
|
||||
nodes: [...state.nodes, customNode],
|
||||
}));
|
||||
return customNode;
|
||||
},
|
||||
updateNodeData: (nodeId, data) => {
|
||||
set((state) => ({
|
||||
|
||||
@@ -150,7 +150,9 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
</label>
|
||||
)}
|
||||
{(isAnyOf || !isConnected) && (
|
||||
<div className={cn(size === "small" ? "pl-2" : "")}>{children}</div>
|
||||
<div className={cn(size === "small" ? "max-w-[340px] pl-2" : "")}>
|
||||
{children}
|
||||
</div>
|
||||
)}{" "}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user