diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewControlPanel.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewControlPanel.tsx index 8c8bbf1842..b4624c85e1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewControlPanel.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewControlPanel.tsx @@ -1,100 +1,30 @@ -// import { Separator } from "@/components/__legacy__/ui/separator"; import { cn } from "@/lib/utils"; 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 { GraphSearchMenu } from "../GraphMenu/GraphMenu"; import { Separator } from "@/components/__legacy__/ui/separator"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import { CustomNode } from "../FlowEditor/nodes/CustomNode/CustomNode"; import { NewSaveControl } from "./NewSaveControl/NewSaveControl"; import { UndoRedoButtons } from "./UndoRedoButtons"; -export type Control = { - icon: React.ReactNode; - label: string; - disabled?: boolean; - onClick: () => void; -}; +export const NewControlPanel = memo(() => { + useNewControlPanel({}); -export type NewControlPanelProps = { - flowExecutionID?: GraphExecutionID | undefined; - visualizeBeads?: "no" | "static" | "animate"; - pinSavePopover?: boolean; - pinBlocksPopover?: boolean; - nodes?: CustomNode[]; - onNodeSelect?: (nodeId: string) => void; - onNodeHover?: (nodeId: string) => void; -}; -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({}); - - return ( -
-
- - {/* - {isGraphSearchEnabled && ( - <> - - - - )} - {controls.map((control, index) => ( - control.onClick()} - data-id={`control-button-${index}`} - data-testid={`blocks-control-${control.label.toLowerCase()}-button`} - disabled={control.disabled || false} - className="rounded-none" - > - {control.icon} - - ))} */} - - - - -
-
- ); - }, -); + return ( +
+
+ + + + + +
+
+ ); +}); export default NewControlPanel; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/GraphMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/GraphMenu.tsx index 8ff96a598b..f732a1a456 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/GraphMenu.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/GraphMenu.tsx @@ -1,4 +1,4 @@ -import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode"; +import { CustomNode } from "../../../FlowEditor/nodes/CustomNode/CustomNode"; import { Popover, PopoverContent, diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/useGraphMenu.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/useGraphMenu.ts index ad903fe35f..271962f2a1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/useGraphMenu.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenu/useGraphMenu.ts @@ -1,5 +1,5 @@ import { useGraphSearch } from "../GraphMenuSearchBar/useGraphMenuSearchBar"; -import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode"; +import { CustomNode } from "../../../FlowEditor/nodes/CustomNode/CustomNode"; interface UseGraphMenuProps { nodes: CustomNode[]; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuContent/GraphContent.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuContent/GraphContent.tsx index 882e18ca66..d45b1e7534 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuContent/GraphContent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuContent/GraphContent.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Separator } from "@/components/__legacy__/ui/separator"; import { ScrollArea } from "@/components/__legacy__/ui/scroll-area"; -import { beautifyString, getPrimaryCategoryColor } from "@/lib/utils"; +import { beautifyString, categoryColorMap } from "@/lib/utils"; import { SearchableNode } from "../GraphMenuSearchBar/useGraphMenuSearchBar"; import { TextRenderer } from "@/components/__legacy__/ui/render"; import { @@ -73,14 +73,12 @@ export const GraphSearchContent: React.FC = ({ } const nodeTitle = - node.data?.metadata?.customized_name || - beautifyString(node.data?.blockType || "").replace( - / Block$/, - "", - ); - const nodeType = beautifyString( - node.data?.blockType || "", - ).replace(/ Block$/, ""); + (node.data?.metadata?.customized_name as string) || + beautifyString(node.data?.title || "").replace(/ Block$/, ""); + const nodeType = beautifyString(node.data?.title || "").replace( + / Block$/, + "", + ); return ( @@ -100,7 +98,13 @@ export const GraphSearchContent: React.FC = ({ onMouseLeave={() => onNodeHover?.(null)} >
@@ -129,9 +133,10 @@ export const GraphSearchContent: React.FC = ({
Node Type: {nodeType}
- {node.data?.metadata?.customized_name && ( + {!!node.data?.metadata?.customized_name && (
- Custom Name: {node.data.metadata.customized_name} + Custom Name:{" "} + {String(node.data.metadata.customized_name)}
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuSearchBar/useGraphMenuSearchBar.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuSearchBar/useGraphMenuSearchBar.tsx index 4342f4ee61..fe9d0c1bae 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuSearchBar/useGraphMenuSearchBar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewSearchGraph/GraphMenuSearchBar/useGraphMenuSearchBar.tsx @@ -1,5 +1,5 @@ import { useState, useMemo, useDeferredValue } from "react"; -import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode"; +import { CustomNode } from "../../../FlowEditor/nodes/CustomNode/CustomNode"; import { beautifyString } from "@/lib/utils"; import jaro from "jaro-winkler"; @@ -67,10 +67,10 @@ function calculateNodeScore( const nodeTitle = (node.data?.title || "").toLowerCase(); // This includes the ID const nodeId = (node.id || "").toLowerCase(); const nodeDescription = (node.data?.description || "").toLowerCase(); - const blockType = (node.data?.blockType || "").toLowerCase(); + const blockType = (node.data?.title || "").toLowerCase(); const beautifiedBlockType = beautifyString(blockType).toLowerCase(); - const customizedName = ( - node.data?.metadata?.customized_name || "" + const customizedName = String( + node.data?.metadata?.customized_name || "", ).toLowerCase(); // Get input and output names with defensive checks diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/useNewControlPanel.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/useNewControlPanel.ts index c80ec1149a..4d06271b68 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/useNewControlPanel.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/useNewControlPanel.ts @@ -1,54 +1,18 @@ -import { GraphID } from "@/lib/autogpt-server-api"; -import { useSearchParams } from "next/navigation"; import { useState } from "react"; export interface NewControlPanelProps { - // flowExecutionID: GraphExecutionID | undefined; visualizeBeads?: "no" | "static" | "animate"; } export const useNewControlPanel = ({ - // flowExecutionID, visualizeBeads: _visualizeBeads, }: NewControlPanelProps) => { const [blockMenuSelected, setBlockMenuSelected] = useState< "save" | "block" | "search" | "" >(""); - const query = useSearchParams(); - const _graphVersion = query.get("flowVersion"); - const _graphVersionParsed = _graphVersion - ? parseInt(_graphVersion) - : undefined; - - const _flowID = (query.get("flowID") as GraphID | null) ?? undefined; - // const { - // agentDescription, - // setAgentDescription, - // saveAgent, - // agentName, - // setAgentName, - // savedAgent, - // isSaving, - // isRunning, - // isStopping, - // } = useAgentGraph( - // flowID, - // graphVersion, - // flowExecutionID, - // visualizeBeads !== "no", - // ); return { blockMenuSelected, setBlockMenuSelected, - // agentDescription, - // setAgentDescription, - // saveAgent, - // agentName, - // setAgentName, - // savedAgent, - // isSaving, - // isRunning, - // isStopping, }; }; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx deleted file mode 100644 index 99b66fe1dc..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx +++ /dev/null @@ -1,443 +0,0 @@ -import React, { useCallback, useMemo, useState, useDeferredValue } from "react"; -import { Card, CardContent, CardHeader } from "@/components/__legacy__/ui/card"; -import { Label } from "@/components/__legacy__/ui/label"; -import { Button } from "@/components/__legacy__/ui/button"; -import { Input } from "@/components/__legacy__/ui/input"; -import { TextRenderer } from "@/components/__legacy__/ui/render"; -import { ScrollArea } from "@/components/__legacy__/ui/scroll-area"; -import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode"; -import { beautifyString } from "@/lib/utils"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/__legacy__/ui/popover"; -import { - Block, - BlockIORootSchema, - BlockUIType, - GraphInputSchema, - GraphOutputSchema, - SpecialBlockID, -} from "@/lib/autogpt-server-api"; -import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons"; -import { IconToyBrick } from "@/components/__legacy__/ui/icons"; -import { getPrimaryCategoryColor } from "@/lib/utils"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/atoms/Tooltip/BaseTooltip"; -import { GraphMeta } from "@/lib/autogpt-server-api"; -import jaro from "jaro-winkler"; -import { getV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs"; -import { okData } from "@/app/api/helpers"; - -type _Block = Omit & { - uiKey?: string; - inputSchema: BlockIORootSchema | GraphInputSchema; - outputSchema: BlockIORootSchema | GraphOutputSchema; - hardcodedValues?: Record; - _cached?: { - blockName: string; - beautifiedName: string; - description: string; - }; -}; - -// Hook to preprocess blocks with cached expensive operations -const useSearchableBlocks = (blocks: _Block[]): _Block[] => { - return useMemo( - () => - blocks.map((block) => { - if (!block._cached) { - block._cached = { - blockName: block.name.toLowerCase(), - beautifiedName: beautifyString(block.name).toLowerCase(), - description: block.description.toLowerCase(), - }; - } - return block; - }), - [blocks], - ); -}; - -interface BlocksControlProps { - blocks: _Block[]; - addBlock: ( - id: string, - name: string, - hardcodedValues: Record, - ) => void; - pinBlocksPopover: boolean; - flows: GraphMeta[]; - nodes: CustomNode[]; -} - -/** - * A React functional component that displays a control for managing blocks. - * - * @component - * @param {Object} BlocksControlProps - The properties for the BlocksControl component. - * @param {Block[]} BlocksControlProps.blocks - An array of blocks to be displayed and filtered. - * @param {(id: string, name: string) => void} BlocksControlProps.addBlock - A function to call when a block is added. - * @returns The rendered BlocksControl component. - */ -export function BlocksControl({ - blocks: _blocks, - addBlock, - pinBlocksPopover, - flows, - nodes, -}: BlocksControlProps) { - const [searchQuery, setSearchQuery] = useState(""); - const deferredSearchQuery = useDeferredValue(searchQuery); - const [selectedCategory, setSelectedCategory] = useState(null); - - const blocks = useSearchableBlocks(_blocks); - - const graphHasWebhookNodes = nodes.some((n) => - [BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(n.data.uiType), - ); - const graphHasInputNodes = nodes.some( - (n) => n.data.uiType == BlockUIType.INPUT, - ); - - const filteredAvailableBlocks = useMemo(() => { - const blockList = blocks - .filter((b) => b.uiType !== BlockUIType.AGENT) - .sort((a, b) => a.name.localeCompare(b.name)); - - // Agent blocks are created from GraphMeta which doesn't include schemas. - // Schemas will be fetched on-demand when the block is actually added. - const agentBlockList = flows - .map((flow): _Block => { - return { - id: SpecialBlockID.AGENT, - name: flow.name, - description: - `Ver.${flow.version}` + - (flow.description ? ` | ${flow.description}` : ""), - categories: [{ category: "AGENT", description: "" }], - // Empty schemas - will be populated when block is added - inputSchema: { type: "object", properties: {} }, - outputSchema: { type: "object", properties: {} }, - staticOutput: false, - uiType: BlockUIType.AGENT, - costs: [], - uiKey: flow.id, - hardcodedValues: { - graph_id: flow.id, - graph_version: flow.version, - // Schemas will be fetched on-demand when block is added - }, - }; - }) - .map( - (agentBlock): _Block => ({ - ...agentBlock, - _cached: { - blockName: agentBlock.name.toLowerCase(), - beautifiedName: beautifyString(agentBlock.name).toLowerCase(), - description: agentBlock.description.toLowerCase(), - }, - }), - ); - - return blockList - .concat(agentBlockList) - .map((block) => ({ - block, - score: blockScoreForQuery(block, deferredSearchQuery), - })) - .filter( - ({ block, score }) => - score > 0 && - (!selectedCategory || - block.categories.some((cat) => cat.category === selectedCategory)), - ) - .sort((a, b) => b.score - a.score) - .map(({ block }) => ({ - ...block, - notAvailable: - (block.uiType == BlockUIType.WEBHOOK && - graphHasWebhookNodes && - "Agents can only have one webhook-triggered block") || - (block.uiType == BlockUIType.WEBHOOK && - graphHasInputNodes && - "Webhook-triggered blocks can't be used together with input blocks") || - (block.uiType == BlockUIType.INPUT && - graphHasWebhookNodes && - "Input blocks can't be used together with a webhook-triggered block") || - null, - })); - }, [ - blocks, - flows, - selectedCategory, - deferredSearchQuery, - graphHasInputNodes, - graphHasWebhookNodes, - ]); - - const resetFilters = useCallback(() => { - setSearchQuery(""); - setSelectedCategory(null); - }, []); - - // Handler to add a block, fetching graph data on-demand for agent blocks - const handleAddBlock = useCallback( - async (block: _Block & { notAvailable: string | null }) => { - if (block.notAvailable) return; - - // For agent blocks, fetch the full graph to get schemas - if (block.uiType === BlockUIType.AGENT && block.hardcodedValues) { - const graphID = block.hardcodedValues.graph_id as string; - const graphVersion = block.hardcodedValues.graph_version as number; - const graphData = okData( - await getV1GetSpecificGraph(graphID, { version: graphVersion }), - ); - - if (graphData) { - addBlock(block.id, block.name, { - ...block.hardcodedValues, - input_schema: graphData.input_schema, - output_schema: graphData.output_schema, - }); - } else { - // Fallback: add without schemas (will be incomplete) - console.error("Failed to fetch graph data for agent block"); - addBlock(block.id, block.name, block.hardcodedValues || {}); - } - } else { - addBlock(block.id, block.name, block.hardcodedValues || {}); - } - }, - [addBlock], - ); - - // Extract unique categories from blocks - const categories = useMemo(() => { - return Array.from( - new Set([ - null, - ...blocks - .flatMap((block) => block.categories.map((cat) => cat.category)) - .sort(), - ]), - ); - }, [blocks]); - - return ( - open || resetFilters()} - > - - - - - - - Blocks - - - - -
- -
-
- - setSearchQuery(e.target.value)} - className="rounded-lg px-8 py-5 dark:bg-slate-800 dark:text-white" - data-id="blocks-control-search-input" - autoComplete="off" - /> -
-
- {categories.map((category) => { - const color = getPrimaryCategoryColor([ - { category: category || "All", description: "" }, - ]); - const colorClass = - selectedCategory === category ? `${color}` : ""; - return ( -
- setSelectedCategory( - selectedCategory === category ? null : category, - ) - } - > - {beautifyString((category || "All").toLowerCase())} -
- ); - })} -
-
- - - {filteredAvailableBlocks.map((block) => ( - { - if (block.notAvailable) return; - e.dataTransfer.effectAllowed = "copy"; - e.dataTransfer.setData( - "application/reactflow", - JSON.stringify({ - blockId: block.id, - blockName: block.name, - hardcodedValues: block?.hardcodedValues || {}, - }), - ); - }} - onClick={() => handleAddBlock(block)} - title={block.notAvailable ?? undefined} - > -
- -
-
- - - - - - -
-
- -
-
-
- ))} -
-
-
-
-
- ); -} - -/** - * Evaluates how well a block matches the search query and returns a relevance score. - * The scoring algorithm works as follows: - * - Returns 1 if no query (all blocks match equally) - * - Normalized query for case-insensitive matching - * - Returns 3 for exact substring matches in block name (highest priority) - * - Returns 2 when all query words appear in the block name (regardless of order) - * - Returns 1.X for blocks with names similar to query using Jaro-Winkler distance (X is similarity score) - * - Returns 0.5 when all query words appear in the block description (lowest priority) - * - Returns 0 for no match - * - * Higher scores will appear first in search results. - */ -function blockScoreForQuery(block: _Block, query: string): number { - if (!query) return 1; - const normalizedQuery = query.toLowerCase().trim(); - const queryWords = normalizedQuery.split(/\s+/); - - // Use cached values for performance - const { blockName, beautifiedName, description } = block._cached!; - - // 1. Exact match in name (highest priority) - if ( - blockName.includes(normalizedQuery) || - beautifiedName.includes(normalizedQuery) - ) { - return 3; - } - - // 2. All query words in name (regardless of order) - const allWordsInName = queryWords.every( - (word) => blockName.includes(word) || beautifiedName.includes(word), - ); - if (allWordsInName) return 2; - - // 3. Similarity with name (Jaro-Winkler) - const similarityThreshold = 0.65; - const nameSimilarity = jaro(blockName, normalizedQuery); - const beautifiedSimilarity = jaro(beautifiedName, normalizedQuery); - const maxSimilarity = Math.max(nameSimilarity, beautifiedSimilarity); - if (maxSimilarity > similarityThreshold) { - return 1 + maxSimilarity; // Score between 1 and 2 - } - - // 4. All query words in description (lower priority) - const allWordsInDescription = queryWords.every((word) => - description.includes(word), - ); - if (allWordsInDescription) return 0.5; - - return 0; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BuildActionBar.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BuildActionBar.tsx deleted file mode 100644 index 9d12439d8d..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BuildActionBar.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from "react"; -import { cn } from "@/lib/utils"; -import { Button } from "@/components/__legacy__/ui/button"; -import { LogOut } from "lucide-react"; -import { ClockIcon, WarningIcon } from "@phosphor-icons/react"; -import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons"; - -interface Props { - onClickAgentOutputs?: () => void; - onClickRunAgent?: () => void; - onClickStopRun: () => void; - onClickScheduleButton?: () => void; - isRunning: boolean; - isDisabled: boolean; - className?: string; - resolutionModeActive?: boolean; -} - -export const BuildActionBar: React.FC = ({ - onClickAgentOutputs, - onClickRunAgent, - onClickStopRun, - onClickScheduleButton, - isRunning, - isDisabled, - className, - resolutionModeActive = false, -}) => { - const buttonClasses = - "flex items-center gap-2 text-sm font-medium md:text-lg"; - - // Show resolution mode message instead of action buttons - if (resolutionModeActive) { - return ( -
-
- - - Remove incompatible connections to continue - -
-
- ); - } - - return ( -
-
- {onClickAgentOutputs && ( - - )} - - {!isRunning ? ( - - ) : ( - - )} - - {onClickScheduleButton && ( - - )} -
-
- ); -}; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ConnectionLine.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ConnectionLine.tsx deleted file mode 100644 index 0a790aedd4..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ConnectionLine.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - BaseEdge, - ConnectionLineComponentProps, - Node, - getBezierPath, - Position, -} from "@xyflow/react"; - -export default function ConnectionLine({ - fromPosition, - fromHandle, - fromX, - fromY, - toPosition, - toX, - toY, -}: ConnectionLineComponentProps) { - const sourceX = - fromPosition === Position.Right - ? fromX + ((fromHandle?.width ?? 0) / 2 - 5) - : fromX - ((fromHandle?.width ?? 0) / 2 - 5); - - const [path] = getBezierPath({ - sourceX: sourceX, - sourceY: fromY, - sourcePosition: fromPosition, - targetX: toX, - targetY: toY, - targetPosition: toPosition, - }); - - return ; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ControlPanel.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ControlPanel.tsx deleted file mode 100644 index ecf4f443d5..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ControlPanel.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Card, CardContent } from "@/components/__legacy__/ui/card"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/atoms/Tooltip/BaseTooltip"; -import { Button } from "@/components/__legacy__/ui/button"; -import { Separator } from "@/components/__legacy__/ui/separator"; -import { cn } from "@/lib/utils"; -import React from "react"; - -/** - * Represents a control element for the ControlPanel Component. - * @type {Object} Control - * @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/ - * @property {string} label - The label of the control, to be leveraged by ToolTip. - * @property {onclick} onClick - The function to be executed when the control is clicked. - */ -export type Control = { - icon: React.ReactNode; - label: string; - disabled?: boolean; - onClick: () => void; -}; - -interface ControlPanelProps { - controls: Control[]; - topChildren?: React.ReactNode; - botChildren?: React.ReactNode; - className?: string; -} - -/** - * ControlPanel component displays a panel with controls as icons.tsx with the ability to take in children. - * @param {Object} ControlPanelProps - The properties of the control panel component. - * @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed. - * @param {Array} ControlPanelProps.children - The child components of the control panel. - * @param {string} ControlPanelProps.className - Additional CSS class names for the control panel. - * @returns The rendered control panel component. - */ -export const ControlPanel = ({ - controls, - topChildren, - botChildren, - className, -}: ControlPanelProps) => { - return ( - - -
- {topChildren} - - {controls.map((control, index) => ( - - -
- -
-
- - {control.label} - -
- ))} - - {botChildren} -
-
-
- ); -}; -export default ControlPanel; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/CustomEdge.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/CustomEdge.tsx deleted file mode 100644 index 5ca5393d69..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/CustomEdge.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { - useCallback, - useContext, - useEffect, - useState, - useRef, -} from "react"; -import { - BaseEdge, - EdgeLabelRenderer, - EdgeProps, - useReactFlow, - XYPosition, - Edge, - Node, -} from "@xyflow/react"; -import "./customedge.css"; -import { X } from "lucide-react"; -import { BuilderContext } from "../Flow/Flow"; -import { NodeExecutionResult } from "@/lib/autogpt-server-api"; -import { useCustomEdge } from "./useCustomEdge"; - -export type CustomEdgeData = { - edgeColor: string; - sourcePos?: XYPosition; - isStatic?: boolean; - beadUp: number; - beadDown: number; - beadData?: Map; -}; - -type Bead = { - t: number; - targetT: number; - startTime: number; -}; - -export type CustomEdge = Edge; - -export function CustomEdge({ - id, - data, - selected, - sourceX, - sourceY, - targetX, - targetY, - markerEnd, -}: EdgeProps) { - const [beads, setBeads] = useState<{ - beads: Bead[]; - created: number; - destroyed: number; - }>({ beads: [], created: 0, destroyed: 0 }); - const beadsRef = useRef(beads); - const { svgPath, length, getPointForT, getTForDistance } = useCustomEdge( - sourceX - 5, - sourceY - 5, - targetX + 3, - targetY - 5, - ); - const { deleteElements } = useReactFlow(); - const builderContext = useContext(BuilderContext); - const { visualizeBeads } = builderContext ?? { - visualizeBeads: "no", - }; - - // Check if this edge is broken (during resolution mode) - const isBroken = - builderContext?.resolutionMode?.active && - builderContext?.resolutionMode?.brokenEdgeIds?.includes(id); - - const onEdgeRemoveClick = () => { - deleteElements({ edges: [{ id }] }); - }; - - const animationDuration = 500; // Duration in milliseconds for bead to travel the curve - const beadDiameter = 12; - const deltaTime = 16; - - const setTargetPositions = useCallback( - (beads: Bead[]) => { - const distanceBetween = Math.min( - (length - beadDiameter) / (beads.length + 1), - beadDiameter, - ); - - return beads.map((bead, index) => { - const distanceFromEnd = beadDiameter * 1.35; - const targetPosition = distanceBetween * index + distanceFromEnd; - const t = getTForDistance(-targetPosition); - - return { - ...bead, - t: visualizeBeads === "animate" ? bead.t : t, - targetT: t, - } as Bead; - }); - }, - [getTForDistance, length, visualizeBeads], - ); - - beadsRef.current = beads; - useEffect(() => { - const beadUp: number = data?.beadUp ?? 0; - const beadDown: number = data?.beadDown ?? 0; - - if ( - beadUp === 0 && - beadDown === 0 && - (beads.created > 0 || beads.destroyed > 0) - ) { - setBeads({ beads: [], created: 0, destroyed: 0 }); - return; - } - - // Add beads - if (beadUp > beads.created) { - setBeads(({ beads, created, destroyed }) => { - const newBeads = []; - for (let i = 0; i < beadUp - created; i++) { - newBeads.push({ t: 0, targetT: 0, startTime: Date.now() }); - } - - const b = setTargetPositions([...beads, ...newBeads]); - return { beads: b, created: beadUp, destroyed }; - }); - } - - // Animate and remove beads - const interval = setInterval( - ({ current: beads }) => { - // If there are no beads visible or moving, stop re-rendering - if ( - (beadUp === beads.created && beads.created === beads.destroyed) || - beads.beads.every((bead) => bead.t >= bead.targetT) - ) { - clearInterval(interval); - return; - } - - setBeads(({ beads, created, destroyed }) => { - let destroyedCount = 0; - - const newBeads = beads - .map((bead) => { - const progressIncrement = deltaTime / animationDuration; - const t = Math.min( - bead.t + bead.targetT * progressIncrement, - bead.targetT, - ); - - return { ...bead, t }; - }) - .filter((bead, index) => { - const removeCount = beadDown - destroyed; - if (bead.t >= bead.targetT && index < removeCount) { - destroyedCount++; - return false; - } - return true; - }); - - return { - beads: setTargetPositions(newBeads), - created, - destroyed: destroyed + destroyedCount, - }; - }); - }, - deltaTime, - beadsRef, - ); - - return () => clearInterval(interval); - }, [data?.beadUp, data?.beadDown, setTargetPositions, visualizeBeads]); - - const middle = getPointForT(0.5); - - // Determine edge color - red for broken edges - const baseColor = data?.edgeColor ?? "#555555"; - const edgeColor = isBroken ? "#ef4444" : baseColor; - // Add opacity to hex color (99 = 60% opacity, 80 = 50% opacity) - const strokeColor = isBroken - ? `${edgeColor}99` - : selected - ? edgeColor - : `${edgeColor}80`; - - return ( - <> - - - -
- -
-
- {beads.beads.map((bead, index) => { - const pos = getPointForT(bead.t); - return ( - - ); - })} - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/customedge.css b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/customedge.css deleted file mode 100644 index 6babb8e770..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/customedge.css +++ /dev/null @@ -1,48 +0,0 @@ -.edge-label-renderer { - position: absolute; - pointer-events: all; -} - -.edge-label-button { - width: 20px; - height: 20px; - background: #eee; - border: 1px solid #fff; - cursor: pointer; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - padding: 0; - color: #555; - opacity: 0; - transition: - opacity 0.2s ease-in-out, - background-color 0.2s ease-in-out; -} - -.edge-label-button.visible { - opacity: 1; -} - -.edge-label-button:hover { - box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08); - background: #f0f0f0; -} - -.edge-label-button svg { - width: 14px; - height: 14px; -} - -.react-flow__edge-interaction { - cursor: pointer; -} - -.react-flow__edges > svg:has(> g.selected) { - z-index: 10 !important; -} - -.react-flow__edgelabel-renderer { - z-index: 11 !important; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/useCustomEdge.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/useCustomEdge.ts deleted file mode 100644 index f8fdeda411..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomEdge/useCustomEdge.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { useCallback, useMemo } from "react"; - -type XYPosition = { - x: number; - y: number; -}; - -export type BezierPath = { - sourcePosition: XYPosition; - control1: XYPosition; - control2: XYPosition; - targetPosition: XYPosition; -}; - -export function useCustomEdge( - sourceX: number, - sourceY: number, - targetX: number, - targetY: number, -) { - const path: BezierPath = useMemo(() => { - const xDifference = Math.abs(sourceX - targetX); - const yDifference = Math.abs(sourceY - targetY); - const xControlDistance = - sourceX < targetX ? 64 : Math.max(xDifference / 2, 64); - const yControlDistance = yDifference < 128 && sourceX > targetX ? -64 : 0; - - return { - sourcePosition: { x: sourceX, y: sourceY }, - control1: { - x: sourceX + xControlDistance, - y: sourceY + yControlDistance, - }, - control2: { - x: targetX - xControlDistance, - y: targetY + yControlDistance, - }, - targetPosition: { x: targetX, y: targetY }, - }; - }, [sourceX, sourceY, targetX, targetY]); - - const svgPath = useMemo( - () => - `M ${path.sourcePosition.x} ${path.sourcePosition.y} ` + - `C ${path.control1.x} ${path.control1.y} ${path.control2.x} ${path.control2.y} ` + - `${path.targetPosition.x}, ${path.targetPosition.y}`, - [path], - ); - - const getPointForT = useCallback( - (t: number) => { - // Bezier formula: (1-t)^3 * p0 + 3*(1-t)^2*t*p1 + 3*(1-t)*t^2*p2 + t^3*p3 - const x = - Math.pow(1 - t, 3) * path.sourcePosition.x + - 3 * Math.pow(1 - t, 2) * t * path.control1.x + - 3 * (1 - t) * Math.pow(t, 2) * path.control2.x + - Math.pow(t, 3) * path.targetPosition.x; - - const y = - Math.pow(1 - t, 3) * path.sourcePosition.y + - 3 * Math.pow(1 - t, 2) * t * path.control1.y + - 3 * (1 - t) * Math.pow(t, 2) * path.control2.y + - Math.pow(t, 3) * path.targetPosition.y; - - return { x, y }; - }, - [path], - ); - - const getArcLength = useCallback( - (t: number, samples: number = 100) => { - let length = 0; - let prevPoint = getPointForT(0); - - for (let i = 1; i <= samples; i++) { - const currT = (i / samples) * t; - const currPoint = getPointForT(currT); - length += Math.sqrt( - Math.pow(currPoint.x - prevPoint.x, 2) + - Math.pow(currPoint.y - prevPoint.y, 2), - ); - prevPoint = currPoint; - } - - return length; - }, - [getPointForT], - ); - - const length = useMemo(() => { - return getArcLength(1); - }, [getArcLength]); - - const getBezierDerivative = useCallback( - (t: number) => { - const mt = 1 - t; - const x = - 3 * - (mt * mt * (path.control1.x - path.sourcePosition.x) + - 2 * mt * t * (path.control2.x - path.control1.x) + - t * t * (path.targetPosition.x - path.control2.x)); - const y = - 3 * - (mt * mt * (path.control1.y - path.sourcePosition.y) + - 2 * mt * t * (path.control2.y - path.control1.y) + - t * t * (path.targetPosition.y - path.control2.y)); - return { x, y }; - }, - [path], - ); - - const getTForDistance = useCallback( - (distance: number, epsilon: number = 0.0001) => { - if (distance < 0) { - distance = length + distance; // If distance is negative, calculate from the end of the curve - } - - let t = distance / getArcLength(1); - let prevT = 0; - - while (Math.abs(t - prevT) > epsilon) { - prevT = t; - const length = getArcLength(t); - const derivative = Math.sqrt( - Math.pow(getBezierDerivative(t).x, 2) + - Math.pow(getBezierDerivative(t).y, 2), - ); - t -= (length - distance) / derivative; - t = Math.max(0, Math.min(1, t)); // Clamp t between 0 and 1 - } - - return t; - }, - [getArcLength, getBezierDerivative, length], - ); - - const getPointAtDistance = useCallback( - (distance: number) => { - if (distance < 0) { - distance = length + distance; // If distance is negative, calculate from the end of the curve - } - - const t = getTForDistance(distance); - return getPointForT(t); - }, - [getTForDistance, getPointForT, length], - ); - - return { - path, - svgPath, - length, - getPointForT, - getTForDistance, - getPointAtDistance, - }; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode.tsx deleted file mode 100644 index 834603cc4a..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode.tsx +++ /dev/null @@ -1,1270 +0,0 @@ -import { getV1GetAyrshareSsoUrl } from "@/app/api/__generated__/endpoints/integrations/integrations"; -import { Input } from "@/components/__legacy__/ui/input"; -import { TextRenderer } from "@/components/__legacy__/ui/render"; -import { Button } from "@/components/atoms/Button/Button"; -import { Switch } from "@/components/atoms/Switch/Switch"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/atoms/Tooltip/BaseTooltip"; -import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip"; -import { toast } from "@/components/molecules/Toast/use-toast"; -import useCredits from "@/hooks/useCredits"; -import { - BlockCost, - BlockIORootSchema, - BlockIOStringSubSchema, - BlockIOSubSchema, - BlockUIType, - Category, - GraphInputSchema, - GraphOutputSchema, - NodeExecutionResult, -} from "@/lib/autogpt-server-api"; -import { - beautifyString, - cn, - fillObjectDefaultsFromSchema, - getPrimaryCategoryColor, - getValue, - hasNonNullNonObjectValue, - isObject, - parseKeys, - setNestedProperty, -} from "@/lib/utils"; -import { InfoIcon, Key } from "@phosphor-icons/react"; -import * as ContextMenu from "@radix-ui/react-context-menu"; -import { - CopyIcon, - DotsVerticalIcon, - ExitIcon, - Pencil1Icon, - TrashIcon, -} from "@radix-ui/react-icons"; -import * as Separator from "@radix-ui/react-separator"; -import { Edge, NodeProps, useReactFlow, Node as XYNode } from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; -import Link from "next/link"; -import React, { - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "react"; -import { Badge } from "@/components/__legacy__/ui/badge"; -import { IconCoin } from "@/components/__legacy__/ui/icons"; -import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert"; -import { BuilderContext } from "../Flow/Flow"; -import { history } from "../history"; -import InputModalComponent from "../InputModalComponent"; -import NodeHandle from "../NodeHandle"; -import { NodeGenericInputField, NodeTextBoxInput } from "../NodeInputs"; -import NodeOutputs from "../NodeOutputs"; -import OutputModalComponent from "../OutputModalComponent"; -import "./customnode.css"; -import { SubAgentUpdateBar } from "./SubAgentUpdateBar"; -import { IncompatibilityDialog } from "./IncompatibilityDialog"; -import { - useSubAgentUpdate, - createUpdatedAgentNodeInputs, - getBrokenEdgeIDs, -} from "../../../hooks/useSubAgentUpdate"; - -export type ConnectedEdge = { - id: string; - source: string; - sourceHandle: string; - target: string; - targetHandle: string; -}; - -export type CustomNodeData = { - blockType: string; - blockCosts: BlockCost[]; - title: string; - description: string; - categories: Category[]; - inputSchema: BlockIORootSchema; - outputSchema: BlockIORootSchema; - hardcodedValues: { [key: string]: any }; - connections: ConnectedEdge[]; - isOutputOpen: boolean; - status?: NodeExecutionResult["status"]; - /** executionResults contains outputs across multiple executions - * with the last element being the most recent output */ - executionResults?: { - execId: string; - data: NodeExecutionResult["output_data"]; - status: NodeExecutionResult["status"]; - }[]; - block_id: string; - backend_id?: string; - errors?: { [key: string]: string }; - isOutputStatic?: boolean; - uiType: BlockUIType; - metadata?: { customized_name?: string; [key: string]: any }; -}; - -export type CustomNode = XYNode; - -export const CustomNode = React.memo( - function CustomNode({ data, id, height, selected }: NodeProps) { - const [isOutputOpen, setIsOutputOpen] = useState( - data.isOutputOpen || false, - ); - const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - const [activeKey, setActiveKey] = useState(null); - const [inputModalValue, setInputModalValue] = useState(""); - const [isOutputModalOpen, setIsOutputModalOpen] = useState(false); - const [isEditingTitle, setIsEditingTitle] = useState(false); - const [customTitle, setCustomTitle] = useState( - data.metadata?.customized_name || "", - ); - const [isTitleHovered, setIsTitleHovered] = useState(false); - const titleInputRef = useRef(null); - const { updateNodeData, deleteElements, addNodes, getNode } = useReactFlow< - CustomNode, - Edge - >(); - const isInitialSetup = useRef(true); - const builderContext = useContext(BuilderContext); - const { formatCredits } = useCredits(); - const [isLoading, setIsLoading] = useState(false); - - let subGraphID = ""; - - if (!builderContext) { - throw new Error( - "BuilderContext consumer must be inside FlowEditor component", - ); - } - - const { - libraryAgent, - setIsAnyModalOpen, - getNextNodeId, - availableFlows, - resolutionMode, - enterResolutionMode, - } = builderContext; - - // Check if this node is in resolution mode (moved up for schema merge logic) - const isInResolutionMode = - resolutionMode.active && resolutionMode.nodeId === id; - - if (data.uiType === BlockUIType.AGENT) { - // Display the graph's schema instead AgentExecutorBlock's schema. - const currentInputSchema = data.hardcodedValues?.input_schema || {}; - const currentOutputSchema = data.hardcodedValues?.output_schema || {}; - subGraphID = data.hardcodedValues?.graph_id || subGraphID; - - // During resolution mode, merge old connected inputs/outputs with new schema - if (isInResolutionMode && resolutionMode.pendingUpdate) { - const newInputSchema = - (resolutionMode.pendingUpdate.input_schema as BlockIORootSchema) || - {}; - const newOutputSchema = - (resolutionMode.pendingUpdate.output_schema as BlockIORootSchema) || - {}; - - // Merge input schemas: start with new schema, add old connected inputs that are missing - const mergedInputProps = { ...newInputSchema.properties }; - const incomp = resolutionMode.incompatibilities; - if (incomp && currentInputSchema.properties) { - // Add back missing inputs that have connections (so user can see/delete them) - incomp.missingInputs.forEach((inputName) => { - if (currentInputSchema.properties[inputName]) { - mergedInputProps[inputName] = - currentInputSchema.properties[inputName]; - } - }); - // Add back inputs with type mismatches (keep old type so connection still works visually) - incomp.inputTypeMismatches.forEach((mismatch) => { - if (currentInputSchema.properties[mismatch.name]) { - mergedInputProps[mismatch.name] = - currentInputSchema.properties[mismatch.name]; - } - }); - } - - // Merge output schemas: start with new schema, add old connected outputs that are missing - const mergedOutputProps = { ...newOutputSchema.properties }; - if (incomp && currentOutputSchema.properties) { - incomp.missingOutputs.forEach((outputName) => { - if (currentOutputSchema.properties[outputName]) { - mergedOutputProps[outputName] = - currentOutputSchema.properties[outputName]; - } - }); - } - - data.inputSchema = { - ...newInputSchema, - properties: mergedInputProps, - }; - data.outputSchema = { - ...newOutputSchema, - properties: mergedOutputProps, - }; - } else { - data.inputSchema = currentInputSchema; - data.outputSchema = currentOutputSchema; - } - } - - const setHardcodedValues = useCallback( - (values: any) => { - updateNodeData(id, { hardcodedValues: values }); - }, - [id, updateNodeData], - ); - - // Sub-agent update detection - const isAgentBlock = data.uiType === BlockUIType.AGENT; - const graphId = isAgentBlock ? data.hardcodedValues?.graph_id : undefined; - const graphVersion = isAgentBlock - ? data.hardcodedValues?.graph_version - : undefined; - - const subAgentUpdate = useSubAgentUpdate( - id, - graphId, - graphVersion, - isAgentBlock - ? (data.hardcodedValues?.input_schema as GraphInputSchema) - : undefined, - isAgentBlock - ? (data.hardcodedValues?.output_schema as GraphOutputSchema) - : undefined, - data.connections, - availableFlows, - ); - - const [showIncompatibilityDialog, setShowIncompatibilityDialog] = - useState(false); - - // Helper to check if a handle is broken (for resolution mode) - const isInputHandleBroken = useCallback( - (handleName: string): boolean => { - if (!isInResolutionMode || !resolutionMode.incompatibilities) { - return false; - } - const incomp = resolutionMode.incompatibilities; - return ( - incomp.missingInputs.includes(handleName) || - incomp.inputTypeMismatches.some((m) => m.name === handleName) - ); - }, - [isInResolutionMode, resolutionMode.incompatibilities], - ); - - const isOutputHandleBroken = useCallback( - (handleName: string): boolean => { - if (!isInResolutionMode || !resolutionMode.incompatibilities) { - return false; - } - return resolutionMode.incompatibilities.missingOutputs.includes( - handleName, - ); - }, - [isInResolutionMode, resolutionMode.incompatibilities], - ); - - // Handle update button click - const handleUpdateClick = useCallback(() => { - if (!subAgentUpdate.latestGraph) return; - - if (subAgentUpdate.isCompatible) { - // Compatible update - directly apply - const updatedValues = createUpdatedAgentNodeInputs( - data.hardcodedValues, - subAgentUpdate.latestGraph, - ); - setHardcodedValues(updatedValues); - toast({ - title: "Agent updated", - description: `Updated to version ${subAgentUpdate.latestVersion}`, - }); - } else { - // Incompatible update - show dialog - setShowIncompatibilityDialog(true); - } - }, [subAgentUpdate, data.hardcodedValues, setHardcodedValues]); - - // Handle confirm incompatible update - const handleConfirmIncompatibleUpdate = useCallback(() => { - if (!subAgentUpdate.latestGraph || !subAgentUpdate.incompatibilities) { - return; - } - - // Create the updated values but DON'T apply them yet - const updatedValues = createUpdatedAgentNodeInputs( - data.hardcodedValues, - subAgentUpdate.latestGraph, - ); - - // Get broken edge IDs - const brokenEdgeIds = getBrokenEdgeIDs( - data.connections, - subAgentUpdate.incompatibilities, - id, - ); - - // Enter resolution mode with pending update (don't apply schema yet) - enterResolutionMode( - id, - subAgentUpdate.incompatibilities, - brokenEdgeIds, - updatedValues, - ); - - setShowIncompatibilityDialog(false); - }, [ - subAgentUpdate, - data.hardcodedValues, - data.connections, - id, - enterResolutionMode, - ]); - - useEffect(() => { - if (data.executionResults || data.status) { - setIsOutputOpen(true); - } - }, [data.executionResults, data.status]); - - useEffect(() => { - setIsOutputOpen(data.isOutputOpen); - }, [data.isOutputOpen]); - - useEffect(() => { - setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen); - }, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]); - - const handleTitleEdit = useCallback(() => { - setIsEditingTitle(true); - setTimeout(() => { - titleInputRef.current?.focus(); - titleInputRef.current?.select(); - }, 0); - }, []); - - const handleTitleSave = useCallback(() => { - setIsEditingTitle(false); - const newMetadata = { - ...data.metadata, - customized_name: customTitle.trim() || undefined, - }; - updateNodeData(id, { metadata: newMetadata }); - }, [customTitle, data.metadata, id, updateNodeData]); - - const handleTitleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleTitleSave(); - } else if (e.key === "Escape") { - setCustomTitle(data.metadata?.customized_name || ""); - setIsEditingTitle(false); - } - }, - [handleTitleSave, data.metadata], - ); - - const displayTitle = - customTitle || - beautifyString(data.blockType?.replace(/Block$/, "") || data.title); - - useEffect(() => { - isInitialSetup.current = false; - if (data.backend_id) return; // don't auto-modify existing nodes - - if (data.uiType === BlockUIType.AGENT) { - setHardcodedValues({ - ...data.hardcodedValues, - inputs: fillObjectDefaultsFromSchema( - data.hardcodedValues.inputs ?? {}, - data.inputSchema, - ), - }); - } else { - setHardcodedValues( - fillObjectDefaultsFromSchema(data.hardcodedValues, data.inputSchema), - ); - } - }, []); - - const setErrors = useCallback( - (errors: { [key: string]: string }) => { - updateNodeData(id, { errors }); - }, - [id, updateNodeData], - ); - - const toggleAdvancedSettings = (checked: boolean) => { - setIsAdvancedOpen(checked); - }; - - const generateOutputHandles = ( - schema: BlockIORootSchema, - nodeType: BlockUIType, - ) => { - if ( - !schema?.properties || - nodeType === BlockUIType.OUTPUT || - nodeType === BlockUIType.NOTE - ) - return null; - - const renderHandles = ( - propSchema: { [key: string]: BlockIOSubSchema }, - keyPrefix = "", - titlePrefix = "", - ) => { - return Object.keys(propSchema).map((propKey) => { - const fieldSchema = propSchema[propKey]; - const fieldTitle = - titlePrefix + (fieldSchema.title || beautifyString(propKey)); - - return ( -
- - {"properties" in fieldSchema && - renderHandles( - fieldSchema.properties, - `${keyPrefix}${propKey}_#_`, - `${fieldTitle}.`, - )} -
- ); - }); - }; - - return renderHandles(schema.properties); - }; - - const generateAyrshareSSOHandles = () => { - 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", - ); - } - } catch (error) { - toast({ - title: "Error", - description: `Error getting SSO URL: ${error}`, - variant: "destructive", - }); - } finally { - setIsLoading(false); - } - }; - - return ( -
- - -
- ); - }; - - const generateInputHandles = ( - schema: BlockIORootSchema, - nodeType: BlockUIType, - ) => { - if (!schema?.properties) return null; - const keys = Object.entries(schema.properties); - switch (nodeType) { - case BlockUIType.NOTE: - // For NOTE blocks, don't render any input handles - const [noteKey, noteSchema] = keys[0]; - return ( -
- -
- ); - - default: - const getInputPropKey = (key: string) => - nodeType == BlockUIType.AGENT ? `inputs.${key}` : key; - - return keys.map(([propKey, propSchema]) => { - const isRequired = data.inputSchema.required?.includes(propKey); - const isAdvanced = propSchema.advanced; - const isHidden = propSchema.hidden; - const isConnectable = - // No input connection handles on INPUT and WEBHOOK blocks - ![ - BlockUIType.INPUT, - BlockUIType.WEBHOOK, - BlockUIType.WEBHOOK_MANUAL, - ].includes(nodeType) && - // No input connection handles for credentials - propKey !== "credentials" && - !propKey.endsWith("_credentials") && - // For OUTPUT blocks, only show the 'value' (hides 'name') input connection handle - !(nodeType == BlockUIType.OUTPUT && propKey == "name"); - const isConnected = isInputHandleConnected(propKey); - - return ( - !isHidden && - (isRequired || isAdvancedOpen || isConnected || !isAdvanced) && ( -
- {isConnectable && - !( - "oneOf" in propSchema && - propSchema.oneOf && - "discriminator" in propSchema && - propSchema.discriminator - ) ? ( - - ) : ( - propKey !== "credentials" && - !propKey.endsWith("_credentials") && ( -
- - {propSchema.title || beautifyString(propKey)} - - -
- ) - )} - {isConnected || ( - - )} -
- ) - ); - }); - } - }; - const handleInputChange = useCallback( - (path: string, value: any) => { - const keys = parseKeys(path); - const newValues = JSON.parse(JSON.stringify(data.hardcodedValues)); - let current = newValues; - - for (let i = 0; i < keys.length - 1; i++) { - const { key: currentKey, index } = keys[i]; - if (index !== undefined) { - if (!current[currentKey]) current[currentKey] = []; - if (!current[currentKey][index]) current[currentKey][index] = {}; - current = current[currentKey][index]; - } else { - if (!current[currentKey]) current[currentKey] = {}; - current = current[currentKey]; - } - } - - const lastKey = keys[keys.length - 1]; - if (lastKey.index !== undefined) { - if (!current[lastKey.key]) current[lastKey.key] = []; - current[lastKey.key][lastKey.index] = value; - } else { - current[lastKey.key] = value; - } - - if (!isInitialSetup.current) { - history.push({ - type: "UPDATE_INPUT", - payload: { nodeId: id, oldValues: data.hardcodedValues, newValues }, - undo: () => setHardcodedValues(data.hardcodedValues), - redo: () => setHardcodedValues(newValues), - }); - } - - setHardcodedValues(newValues); - const errors = data.errors || {}; - // Remove error with the same key - setNestedProperty(errors, path, null); - setErrors({ ...errors }); - }, - [data.hardcodedValues, id, setHardcodedValues, data.errors, setErrors], - ); - - const isInputHandleConnected = (key: string) => { - return ( - data.connections && - data.connections.some((conn: any) => { - if (typeof conn === "string") { - const [_source, target] = conn.split(" -> "); - return target.includes(key) && target.includes(data.title); - } - return conn.target === id && conn.targetHandle === key; - }) - ); - }; - - const isOutputHandleConnected = (key: string) => { - return ( - data.connections && - data.connections.some((conn: any) => { - if (typeof conn === "string") { - const [source, _target] = conn.split(" -> "); - return source.includes(key) && source.includes(data.title); - } - return conn.source === id && conn.sourceHandle === key; - }) - ); - }; - - const handleInputClick = useCallback( - (key: string) => { - console.debug(`Opening modal for key: ${key}`); - setActiveKey(key); - const value = getValue(key, data.hardcodedValues); - setInputModalValue( - typeof value === "object" ? JSON.stringify(value, null, 2) : value, - ); - setIsModalOpen(true); - }, - [data.hardcodedValues], - ); - - const handleModalSave = useCallback( - (value: string) => { - if (activeKey) { - try { - const parsedValue = JSON.parse(value); - // Validate that the parsed value is safe before using it - if (isObject(parsedValue) || Array.isArray(parsedValue)) { - handleInputChange(activeKey, parsedValue); - } else { - // For primitive values, use the original string - handleInputChange(activeKey, value); - } - } catch { - // If JSON parsing fails, treat as plain text - handleInputChange(activeKey, value); - } - } - setIsModalOpen(false); - setActiveKey(null); - }, - [activeKey, handleInputChange], - ); - - const handleOutputClick = () => { - setIsOutputModalOpen(true); - }; - - const deleteNode = useCallback(() => { - console.debug("Deleting node:", id); - - // Remove the node - deleteElements({ nodes: [{ id }] }); - }, [id, deleteElements]); - - const copyNode = useCallback(() => { - const newId = getNextNodeId(); - const currentNode = getNode(id); - - if (!currentNode) { - console.error("Cannot copy node: current node not found"); - return; - } - - const verticalOffset = height ?? 100; - - const newNode: CustomNode = { - id: newId, - type: currentNode.type, - position: { - x: currentNode.position.x, - y: currentNode.position.y - verticalOffset - 20, - }, - data: { - ...data, - title: `${data.title} (Copy)`, - block_id: data.block_id, - connections: [], - isOutputOpen: false, - metadata: { - ...data.metadata, - customized_name: undefined, // Don't copy the custom name - }, - }, - }; - - addNodes(newNode); - - history.push({ - type: "ADD_NODE", - payload: { node: { ...newNode, ...newNode.data } as CustomNodeData }, - undo: () => deleteElements({ nodes: [{ id: newId }] }), - redo: () => addNodes(newNode), - }); - }, [id, data, height, addNodes, deleteElements, getNode, getNextNodeId]); - - const hasConfigErrors = - data.errors && hasNonNullNonObjectValue(data.errors); - const outputData = data.executionResults?.at(-1)?.data; - const hasOutputError = - typeof outputData === "object" && - outputData !== null && - "error" in outputData; - - useEffect(() => { - if (hasConfigErrors) { - const filteredErrors = Object.fromEntries( - Object.entries(data.errors || {}).filter(([, value]) => - hasNonNullNonObjectValue(value), - ), - ); - console.error( - "Block configuration errors for", - data.title, - ":", - filteredErrors, - ); - } - if (hasOutputError) { - console.error( - "Block output contains error for", - data.title, - ":", - outputData.error, - ); - } - }, [hasConfigErrors, hasOutputError, data.errors, outputData, data.title]); - - const blockClasses = [ - "custom-node", - "dark-theme", - "rounded-xl", - "bg-white/[.9] dark:bg-gray-800/[.9]", - "border border-gray-300 dark:border-gray-600", - data.uiType === BlockUIType.NOTE ? "w-[300px]" : "w-[500px]", - data.uiType === BlockUIType.NOTE - ? "bg-yellow-100 dark:bg-yellow-900" - : "bg-white dark:bg-gray-800", - selected ? "shadow-2xl" : "", - ] - .filter(Boolean) - .join(" "); - - const errorClass = - hasConfigErrors || hasOutputError - ? "border-red-200 dark:border-red-800 border-2" - : ""; - - const statusClass = (() => { - if (hasConfigErrors || hasOutputError) - return "border-red-200 dark:border-red-800 border-4"; - switch (data.status?.toLowerCase()) { - case "completed": - return "border-green-200 dark:border-green-800 border-4"; - case "running": - return "border-yellow-200 dark:border-yellow-800 border-4"; - case "failed": - return "border-red-200 dark:border-red-800 border-4"; - case "incomplete": - return "border-purple-200 dark:border-purple-800 border-4"; - case "queued": - return "border-cyan-200 dark:border-cyan-800 border-4"; - case "review": - return "border-orange-200 dark:border-orange-800 border-4"; - default: - return ""; - } - })(); - - const statusBackgroundClass = (() => { - if (hasConfigErrors || hasOutputError) - return "bg-red-200 dark:bg-red-800"; - switch (data.status?.toLowerCase()) { - case "completed": - return "bg-green-200 dark:bg-green-800"; - case "running": - return "bg-yellow-200 dark:bg-yellow-800"; - case "failed": - return "bg-red-200 dark:bg-red-800"; - case "incomplete": - return "bg-purple-200 dark:bg-purple-800"; - case "queued": - return "bg-cyan-200 dark:bg-cyan-800"; - case "review": - return "bg-orange-200 dark:bg-orange-800"; - default: - return ""; - } - })(); - - const hasAdvancedFields = - data.inputSchema?.properties && - Object.entries(data.inputSchema.properties).some(([key, value]) => { - return ( - value.advanced === true && !data.inputSchema.required?.includes(key) - ); - }); - - const inputValues = data.hardcodedValues; - - const isCostFilterMatch = (costFilter: any, inputValues: any): boolean => { - /* - Filter rules: - - If costFilter is an object, then check if costFilter is the subset of inputValues - - Otherwise, check if costFilter is equal to inputValues. - - Undefined, null, and empty string are considered as equal. - */ - return typeof costFilter === "object" && typeof inputValues === "object" - ? Object.entries(costFilter).every( - ([k, v]) => - (!v && !inputValues[k]) || isCostFilterMatch(v, inputValues[k]), - ) - : costFilter === inputValues; - }; - - const blockCost = - data.blockCosts && - data.blockCosts.find((cost) => - isCostFilterMatch(cost.cost_filter, inputValues), - ); - - const LineSeparator = () => ( -
- -
- ); - - const ContextMenuContent = () => ( - - - - Copy - - {subGraphID && ( - window.open(`/build?flowID=${subGraphID}`)} - className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" - > - - Open agent - - )} - - - - Delete - - - ); - - const onContextButtonTrigger = (e: React.MouseEvent) => { - e.preventDefault(); - const rect = e.currentTarget.getBoundingClientRect(); - const event = new MouseEvent("contextmenu", { - bubbles: true, - clientX: rect.left + rect.width / 2, - clientY: rect.top + rect.height / 2, - }); - e.currentTarget.dispatchEvent(event); - }; - - const stripeColor = getPrimaryCategoryColor(data.categories); - - const nodeContent = () => ( -
- {/* Header */} -
- {/* Color Stripe */} -
- -
-
-
setIsTitleHovered(true)} - onMouseLeave={() => setIsTitleHovered(false)} - > - {isEditingTitle ? ( - setCustomTitle(e.target.value)} - onBlur={handleTitleSave} - onKeyDown={handleTitleKeyDown} - className="h-7 w-auto min-w-[100px] max-w-[200px] px-2 py-1 text-lg font-semibold" - placeholder={beautifyString( - data.blockType?.replace(/Block$/, "") || data.title, - )} - /> - ) : ( - - -

- -

-
- {customTitle && ( - -

- Type:{" "} - {beautifyString( - data.blockType?.replace(/Block$/, "") || data.title, - )} -

-
- )} -
- )} - {isTitleHovered && !isEditingTitle && ( - - )} -
- - #{(data.backend_id || id).split("-")[0]} - - -
- - -
-
- {blockCost && ( -
- - {" "} - - {formatCredits(blockCost.cost_amount)} - - {" \/ "} - {blockCost.cost_type} - -
- )} - {data.categories.map((category) => ( - - {beautifyString(category.category.toLowerCase())} - - ))} -
-
- - -
- - {/* Sub-agent Update Bar - shown below header */} - {isAgentBlock && (subAgentUpdate.hasUpdate || isInResolutionMode) && ( - - )} - - {/* Body */} -
- {/* Input Handles */} - {data.uiType !== BlockUIType.NOTE ? ( -
-
- {data.uiType === BlockUIType.AYRSHARE ? ( - <> - {generateAyrshareSSOHandles()} - {generateInputHandles( - data.inputSchema, - BlockUIType.STANDARD, - )} - - ) : [BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes( - data.uiType, - ) ? ( - <> - - - - - You can set up and manage this trigger in your{" "} - - Agent Library - - {!data.backend_id && " (after saving the graph)"}. - - - -
- {generateInputHandles(data.inputSchema, data.uiType)} -
- - ) : ( - data.inputSchema && - generateInputHandles(data.inputSchema, data.uiType) - )} -
-
- ) : ( -
- {data.inputSchema && - generateInputHandles(data.inputSchema, data.uiType)} -
- )} - - {/* Advanced Settings */} - {data.uiType !== BlockUIType.NOTE && hasAdvancedFields && ( - <> - -
- Advanced - -
- - )} - {/* Output Handles */} - {data.uiType !== BlockUIType.NOTE && ( - <> - -
-
- {data.outputSchema && - generateOutputHandles(data.outputSchema, data.uiType)} -
-
- - )} -
- {/* End Body */} - {/* Footer */} -
- {/* Display Outputs */} - {isOutputOpen && data.uiType !== BlockUIType.NOTE && ( -
- {(data.executionResults?.length ?? 0) > 0 ? ( -
- - -
- -
-
- ) : ( -
- )} -
- - {hasConfigErrors || hasOutputError - ? "Error" - : data.status - ? beautifyString(data.status) - : "Not Run"} - -
-
- )} -
- setIsModalOpen(false)} - onSave={handleModalSave} - defaultValue={inputModalValue} - key={activeKey} - /> - setIsOutputModalOpen(false)} - executionResults={data.executionResults?.toReversed() || []} - /> -
- ); - - return ( - <> - - {nodeContent()} - - - {/* Incompatibility Dialog for sub-agent updates */} - {isAgentBlock && subAgentUpdate.incompatibilities && ( - setShowIncompatibilityDialog(false)} - onConfirm={handleConfirmIncompatibleUpdate} - currentVersion={subAgentUpdate.currentVersion} - latestVersion={subAgentUpdate.latestVersion} - agentName={data.blockType || "Agent"} - incompatibilities={subAgentUpdate.incompatibilities} - /> - )} - - ); - }, - (prevProps, nextProps) => { - // Only re-render if the 'data' prop has changed - return prevProps.data === nextProps.data; - }, -); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/IncompatibilityDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/IncompatibilityDialog.tsx deleted file mode 100644 index 951fe2eab5..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/IncompatibilityDialog.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/__legacy__/ui/dialog"; -import { Button } from "@/components/__legacy__/ui/button"; -import { AlertTriangle, XCircle, PlusCircle } from "lucide-react"; -import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types"; -import { beautifyString } from "@/lib/utils"; -import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert"; - -interface IncompatibilityDialogProps { - isOpen: boolean; - onClose: () => void; - onConfirm: () => void; - currentVersion: number; - latestVersion: number; - agentName: string; - incompatibilities: IncompatibilityInfo; -} - -export const IncompatibilityDialog: React.FC = ({ - isOpen, - onClose, - onConfirm, - currentVersion, - latestVersion, - agentName, - incompatibilities, -}) => { - const hasMissingInputs = incompatibilities.missingInputs.length > 0; - const hasMissingOutputs = incompatibilities.missingOutputs.length > 0; - const hasNewInputs = incompatibilities.newInputs.length > 0; - const hasNewOutputs = incompatibilities.newOutputs.length > 0; - const hasNewRequired = incompatibilities.newRequiredInputs.length > 0; - const hasTypeMismatches = incompatibilities.inputTypeMismatches.length > 0; - - const hasInputChanges = hasMissingInputs || hasNewInputs; - const hasOutputChanges = hasMissingOutputs || hasNewOutputs; - - return ( - !open && onClose()}> - - - - - Incompatible Update - - - Updating {beautifyString(agentName)} from v - {currentVersion} to v{latestVersion} will break some connections. - - - -
- {/* Input changes - two column layout */} - {hasInputChanges && ( - } - leftTitle="Removed" - leftItems={incompatibilities.missingInputs} - rightIcon={} - rightTitle="Added" - rightItems={incompatibilities.newInputs} - /> - )} - - {/* Output changes - two column layout */} - {hasOutputChanges && ( - } - leftTitle="Removed" - leftItems={incompatibilities.missingOutputs} - rightIcon={} - rightTitle="Added" - rightItems={incompatibilities.newOutputs} - /> - )} - - {hasTypeMismatches && ( - } - title="Type Changed" - description="These connected inputs have a different type:" - items={incompatibilities.inputTypeMismatches.map( - (m) => `${m.name} (${m.oldType} → ${m.newType})`, - )} - /> - )} - - {hasNewRequired && ( - } - title="New Required Inputs" - description="These inputs are now required:" - items={incompatibilities.newRequiredInputs} - /> - )} -
- - - - If you proceed, you'll need to remove the broken connections - before you can save or run your agent. - - - - - - - -
-
- ); -}; - -interface TwoColumnSectionProps { - title: string; - leftIcon: React.ReactNode; - leftTitle: string; - leftItems: string[]; - rightIcon: React.ReactNode; - rightTitle: string; - rightItems: string[]; -} - -const TwoColumnSection: React.FC = ({ - title, - leftIcon, - leftTitle, - leftItems, - rightIcon, - rightTitle, - rightItems, -}) => ( -
- {title} -
- {/* Left column - Breaking changes */} -
-
- {leftIcon} - {leftTitle} -
-
    - {leftItems.length > 0 ? ( - leftItems.map((item) => ( -
  • - - {item} - -
  • - )) - ) : ( -
  • - None -
  • - )} -
-
- - {/* Right column - Possible solutions */} -
-
- {rightIcon} - {rightTitle} -
-
    - {rightItems.length > 0 ? ( - rightItems.map((item) => ( -
  • - - {item} - -
  • - )) - ) : ( -
  • - None -
  • - )} -
-
-
-
-); - -interface SingleColumnSectionProps { - icon: React.ReactNode; - title: string; - description: string; - items: string[]; -} - -const SingleColumnSection: React.FC = ({ - icon, - title, - description, - items, -}) => ( -
-
- {icon} - {title} -
-

- {description} -

-
    - {items.map((item) => ( -
  • - - {item} - -
  • - ))} -
-
-); - -export default IncompatibilityDialog; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/SubAgentUpdateBar.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/SubAgentUpdateBar.tsx deleted file mode 100644 index 5f421d90d8..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/SubAgentUpdateBar.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from "react"; -import { Button } from "@/components/__legacy__/ui/button"; -import { ArrowUp, AlertTriangle, Info } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/atoms/Tooltip/BaseTooltip"; -import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types"; -import { cn } from "@/lib/utils"; - -interface SubAgentUpdateBarProps { - currentVersion: number; - latestVersion: number; - isCompatible: boolean; - incompatibilities: IncompatibilityInfo | null; - onUpdate: () => void; - isInResolutionMode?: boolean; -} - -export const SubAgentUpdateBar: React.FC = ({ - currentVersion, - latestVersion, - isCompatible, - incompatibilities, - onUpdate, - isInResolutionMode = false, -}) => { - if (isInResolutionMode) { - return ; - } - - return ( -
-
- - - Update available (v{currentVersion} → v{latestVersion}) - - {!isCompatible && ( - - - - - -

Incompatible changes detected

-

- Click Update to see details -

-
-
- )} -
- -
- ); -}; - -interface ResolutionModeBarProps { - incompatibilities: IncompatibilityInfo | null; -} - -const ResolutionModeBar: React.FC = ({ - incompatibilities, -}) => { - const formatIncompatibilities = () => { - if (!incompatibilities) return "No incompatibilities"; - - const items: string[] = []; - - if (incompatibilities.missingInputs.length > 0) { - items.push( - `Missing inputs: ${incompatibilities.missingInputs.join(", ")}`, - ); - } - if (incompatibilities.missingOutputs.length > 0) { - items.push( - `Missing outputs: ${incompatibilities.missingOutputs.join(", ")}`, - ); - } - if (incompatibilities.newRequiredInputs.length > 0) { - items.push( - `New required inputs: ${incompatibilities.newRequiredInputs.join(", ")}`, - ); - } - if (incompatibilities.inputTypeMismatches.length > 0) { - const mismatches = incompatibilities.inputTypeMismatches - .map((m) => `${m.name} (${m.oldType} → ${m.newType})`) - .join(", "); - items.push(`Type changed: ${mismatches}`); - } - - return items.join("\n"); - }; - - return ( -
-
- - - Remove incompatible connections - - - - - - -

Incompatible changes:

-

{formatIncompatibilities()}

-

- Delete the red connections to continue -

-
-
-
-
- ); -}; - -export default SubAgentUpdateBar; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/customnode.css b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/customnode.css deleted file mode 100644 index eebd798095..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/CustomNode/customnode.css +++ /dev/null @@ -1,131 +0,0 @@ -.custom-node { - color: #000000; - box-sizing: border-box; - transition: border-color 0.3s ease-in-out; -} - -.custom-node .custom-switch { - padding: 0.5rem 1.25rem; - display: flex; - align-items: center; - justify-content: space-between; -} - -.error-message { - color: #d9534f; - font-size: 13px; - padding-left: 0.5rem; -} - -/* Existing styles */ -.handle-container { - display: flex; - position: relative; - margin-bottom: 0px; - padding: 5px; - min-height: 44px; - height: 100%; -} - -.react-flow__handle { - background: transparent; - width: auto; - height: auto; - border: 0; - position: relative; - transform: none; -} - -.border-error { - border: 1px solid #d9534f; -} - -.select-input { - width: 100%; - padding: 5px; - border-radius: 4px; - border: 1px solid #000; - background: #fff; - color: #000; -} - -.radio-label { - display: block; - margin: 5px 0; - color: #000; -} - -.number-input { - width: 100%; - padding: 5px; - border-radius: 4px; - background: #fff; - color: #000; -} - -.array-item-container { - display: flex; - align-items: center; - margin-bottom: 5px; -} - -.array-item-input { - flex-grow: 1; - padding: 5px; - border-radius: 4px; - border: 1px solid #000; - background: #fff; - color: #000; -} - -.array-item-remove { - background: #d9534f; - border: none; - color: white; - cursor: pointer; - margin-left: 5px; - border-radius: 4px; - padding: 5px 10px; -} - -.array-item-add { - background: #5bc0de; - border: none; - color: white; - cursor: pointer; - border-radius: 4px; - padding: 5px 10px; - margin-top: 5px; -} - -.error-message { - color: #d9534f; - font-size: 13px; - margin-top: 5px; - margin-left: 5px; -} - -/* Styles for node states */ -.completed { - border-color: #27ae60; /* Green border for completed nodes */ -} - -.running { - border-color: #f39c12; /* Orange border for running nodes */ -} - -.failed { - border-color: #c0392b; /* Red border for failed nodes */ -} - -.incomplete { - border-color: #9f14ab; /* Pink border for incomplete nodes */ -} - -.queued { - border-color: #25e6e6; /* Cyan border for queued nodes */ -} - -.custom-switch { - padding-left: 2px; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/DataTable.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/DataTable.tsx deleted file mode 100644 index c58bdac642..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/DataTable.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { beautifyString } from "@/lib/utils"; -import { Clipboard, Maximize2 } from "lucide-react"; -import React, { useMemo, useState } from "react"; -import { Button } from "../../../../../components/__legacy__/ui/button"; -import { ContentRenderer } from "../../../../../components/__legacy__/ui/render"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "../../../../../components/__legacy__/ui/table"; -import type { OutputMetadata } from "@/components/contextual/OutputRenderers"; -import { - globalRegistry, - OutputItem, -} from "@/components/contextual/OutputRenderers"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import { useToast } from "../../../../../components/molecules/Toast/use-toast"; -import ExpandableOutputDialog from "./ExpandableOutputDialog"; - -type DataTableProps = { - title?: string; - truncateLongData?: boolean; - data: { [key: string]: Array }; -}; - -export default function DataTable({ - title, - truncateLongData, - data, -}: DataTableProps) { - const { toast } = useToast(); - const enableEnhancedOutputHandling = useGetFlag( - Flag.ENABLE_ENHANCED_OUTPUT_HANDLING, - ); - const [expandedDialog, setExpandedDialog] = useState<{ - isOpen: boolean; - execId: string; - pinName: string; - data: any[]; - } | null>(null); - - // Prepare renderers for each item when enhanced mode is enabled - const getItemRenderer = useMemo(() => { - if (!enableEnhancedOutputHandling) return null; - return (item: unknown) => { - const metadata: OutputMetadata = {}; - return globalRegistry.getRenderer(item, metadata); - }; - }, [enableEnhancedOutputHandling]); - - const copyData = (pin: string, data: string) => { - navigator.clipboard.writeText(data).then(() => { - toast({ - title: `"${pin}" data copied to clipboard!`, - duration: 2000, - }); - }); - }; - - const openExpandedView = (pinName: string, pinData: any[]) => { - setExpandedDialog({ - isOpen: true, - execId: title || "Unknown Execution", - pinName, - data: pinData, - }); - }; - - const closeExpandedView = () => { - setExpandedDialog(null); - }; - - return ( - <> - {title && {title}} - - - - Pin - Data - - - - {Object.entries(data).map(([key, value]) => ( - - - {beautifyString(key)} - - -
-
- - -
- {value.map((item, index) => { - const renderer = getItemRenderer?.(item); - if (enableEnhancedOutputHandling && renderer) { - const metadata: OutputMetadata = {}; - return ( - - - {index < value.length - 1 && ", "} - - ); - } - return ( - - - {index < value.length - 1 && ", "} - - ); - })} -
-
-
- ))} -
-
- - {expandedDialog && ( - - )} - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ExpandableOutputDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ExpandableOutputDialog.tsx deleted file mode 100644 index 1ccb3d1261..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/ExpandableOutputDialog.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import type { OutputMetadata } from "@/components/contextual/OutputRenderers"; -import { - globalRegistry, - OutputActions, - OutputItem, -} from "@/components/contextual/OutputRenderers"; -import { Dialog } from "@/components/molecules/Dialog/Dialog"; -import { beautifyString } from "@/lib/utils"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import { Clipboard, Maximize2 } from "lucide-react"; -import React, { FC, useMemo, useState } from "react"; -import { Button } from "../../../../../components/__legacy__/ui/button"; -import { ContentRenderer } from "../../../../../components/__legacy__/ui/render"; -import { ScrollArea } from "../../../../../components/__legacy__/ui/scroll-area"; -import { Separator } from "../../../../../components/__legacy__/ui/separator"; -import { Switch } from "../../../../../components/atoms/Switch/Switch"; -import { useToast } from "../../../../../components/molecules/Toast/use-toast"; - -interface ExpandableOutputDialogProps { - isOpen: boolean; - onClose: () => void; - execId: string; - pinName: string; - data: any[]; -} - -const ExpandableOutputDialog: FC = ({ - isOpen, - onClose, - execId, - pinName, - data, -}) => { - const { toast } = useToast(); - const enableEnhancedOutputHandling = useGetFlag( - Flag.ENABLE_ENHANCED_OUTPUT_HANDLING, - ); - const [useEnhancedRenderer, setUseEnhancedRenderer] = useState(false); - - // Prepare items for the enhanced renderer system - const outputItems = useMemo(() => { - if (!data || !useEnhancedRenderer) return []; - - const items: Array<{ - key: string; - label: string; - value: unknown; - metadata?: OutputMetadata; - renderer: any; - }> = []; - - data.forEach((value, index) => { - const metadata: OutputMetadata = {}; - - // Extract metadata from the value if it's an object - if ( - typeof value === "object" && - value !== null && - !React.isValidElement(value) - ) { - const objValue = value as any; - if (objValue.type) metadata.type = objValue.type; - if (objValue.mimeType) metadata.mimeType = objValue.mimeType; - if (objValue.filename) metadata.filename = objValue.filename; - if (objValue.language) metadata.language = objValue.language; - } - - const renderer = globalRegistry.getRenderer(value, metadata); - if (renderer) { - items.push({ - key: `item-${index}`, - label: index === 0 ? beautifyString(pinName) : "", - value, - metadata, - renderer, - }); - } else { - // Fallback to text renderer - const textRenderer = globalRegistry - .getAllRenderers() - .find((r) => r.name === "TextRenderer"); - if (textRenderer) { - items.push({ - key: `item-${index}`, - label: index === 0 ? beautifyString(pinName) : "", - value: - typeof value === "string" - ? value - : JSON.stringify(value, null, 2), - metadata, - renderer: textRenderer, - }); - } - } - }); - - return items; - }, [data, useEnhancedRenderer, pinName]); - - const copyData = () => { - const formattedData = data - .map((item) => - typeof item === "object" ? JSON.stringify(item, null, 2) : String(item), - ) - .join("\n\n"); - - navigator.clipboard.writeText(formattedData).then(() => { - toast({ - title: `"${beautifyString(pinName)}" data copied to clipboard!`, - duration: 2000, - }); - }); - }; - - return ( - -
- - Full Output Preview -
- {enableEnhancedOutputHandling && ( -
- - -
- )} -
- } - controlled={{ - isOpen, - set: (open) => { - if (!open) onClose(); - }, - }} - onClose={onClose} - styling={{ - maxWidth: "56rem", - width: "90vw", - height: "90vh", - }} - > - -
-
-

- Execution ID: {execId} -
- Pin:{" "} - {beautifyString(pinName)} -

-
- -
- {useEnhancedRenderer && outputItems.length > 0 && ( -
- ({ - value: item.value, - metadata: item.metadata, - renderer: item.renderer, - }))} - /> -
- )} - -
- {data.length > 0 ? ( - useEnhancedRenderer ? ( -
- {outputItems.map((item) => ( - - ))} -
- ) : ( -
- {data.map((item, index) => ( -
-
- - Item {index + 1} of {data.length} - - -
- -
- -
-
- ))} -
- ) - ) : ( -
- No data available -
- )} -
-
-
- - -
- {data.length} item{data.length !== 1 ? "s" : ""} total -
-
- {!useEnhancedRenderer && ( - - )} - -
-
-
-
- - ); -}; - -export default ExpandableOutputDialog; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx deleted file mode 100644 index 67b3cad9af..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx +++ /dev/null @@ -1,1193 +0,0 @@ -"use client"; -import React, { - createContext, - useState, - useCallback, - useEffect, - useMemo, - useRef, - MouseEvent, - Suspense, -} from "react"; -import Link from "next/link"; -import { - ReactFlow, - ReactFlowProvider, - Controls, - Background, - Node, - OnConnect, - Connection, - MarkerType, - NodeChange, - EdgeChange, - useReactFlow, - applyEdgeChanges, - applyNodeChanges, -} from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; -import { ConnectedEdge, CustomNode } from "../CustomNode/CustomNode"; -import "./flow.css"; -import { - BlockIORootSchema, - BlockUIType, - formatEdgeID, - GraphExecutionID, - GraphID, - GraphMeta, - LibraryAgent, - SpecialBlockID, -} from "@/lib/autogpt-server-api"; -import { getV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs"; -import { okData } from "@/app/api/helpers"; -import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types"; -import { Key, storage } from "@/services/storage/local-storage"; -import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils"; -import { history } from "../history"; -import { CustomEdge } from "../CustomEdge/CustomEdge"; -import ConnectionLine from "../ConnectionLine"; -import { - Control, - ControlPanel, -} from "@/app/(platform)/build/components/legacy-builder/ControlPanel"; -import { SaveControl } from "@/app/(platform)/build/components/legacy-builder/SaveControl"; -import { BlocksControl } from "@/app/(platform)/build/components/legacy-builder/BlocksControl"; -import { GraphSearchControl } from "@/app/(platform)/build/components/legacy-builder/GraphSearchControl"; -import { IconUndo2, IconRedo2 } from "@/components/__legacy__/ui/icons"; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/molecules/Alert/Alert"; -import { startTutorial } from "../tutorial"; -import useAgentGraph from "@/hooks/useAgentGraph"; -import { v4 as uuidv4 } from "uuid"; -import { useRouter, usePathname, useSearchParams } from "next/navigation"; -import RunnerUIWrapper, { RunnerUIWrapperRef } from "../RunnerUIWrapper"; -import OttoChatWidget from "@/app/(platform)/build/components/legacy-builder/OttoChatWidget"; -import { useToast } from "@/components/molecules/Toast/use-toast"; -import { useCopyPaste } from "../useCopyPaste"; -import NewControlPanel from "@/app/(platform)/build/components/NewControlPanel/NewControlPanel"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import { BuildActionBar } from "../BuildActionBar"; -import { FloatingReviewsPanel } from "@/components/organisms/FloatingReviewsPanel/FloatingReviewsPanel"; -import { useFlowRealtime } from "@/app/(platform)/build/components/FlowEditor/Flow/useFlowRealtime"; -import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle"; - -// This is for the history, this is the minimum distance a block must move before it is logged -// It helps to prevent spamming the history with small movements especially when pressing on a input in a block -const MINIMUM_MOVE_BEFORE_LOG = 50; - -export type ResolutionModeState = { - active: boolean; - nodeId: string | null; - incompatibilities: IncompatibilityInfo | null; - brokenEdgeIds: string[]; - pendingUpdate: Record | null; // The hardcoded values to apply after resolution -}; - -type BuilderContextType = { - libraryAgent: LibraryAgent | null; - visualizeBeads: "no" | "static" | "animate"; - setIsAnyModalOpen: (isOpen: boolean) => void; - getNextNodeId: () => string; - getNodeTitle: (nodeID: string) => string | null; - availableFlows: GraphMeta[]; - resolutionMode: ResolutionModeState; - enterResolutionMode: ( - nodeId: string, - incompatibilities: IncompatibilityInfo, - brokenEdgeIds: string[], - pendingUpdate: Record, - ) => void; - exitResolutionMode: () => void; - applyPendingUpdate: () => void; -}; - -export type NodeDimension = { - [nodeId: string]: { - x: number; - y: number; - width: number; - height: number; - }; -}; - -export const BuilderContext = createContext(null); - -const FlowEditor: React.FC<{ - flowID?: GraphID; - flowVersion?: number; - className?: string; -}> = ({ flowID, flowVersion, className }) => { - const { - addNodes, - addEdges, - getNode, - deleteElements, - updateNode, - getViewport, - setViewport, - fitView, - screenToFlowPosition, - } = useReactFlow(); - const [nodeId, setNodeId] = useState(1); - const [isAnyModalOpen, setIsAnyModalOpen] = useState(false); - const [visualizeBeads] = useState<"no" | "static" | "animate">("animate"); - const [flowExecutionID, setFlowExecutionID] = useState< - GraphExecutionID | undefined - >(); - // State to control if blocks menu should be pinned open - const [pinBlocksPopover, setPinBlocksPopover] = useState(false); - // State to control if save popover should be pinned open - const [pinSavePopover, setPinSavePopover] = useState(false); - const [hasAutoFramed, setHasAutoFramed] = useState(false); - - const { - agentName, - setAgentName, - agentDescription, - setAgentDescription, - agentRecommendedScheduleCron, - setAgentRecommendedScheduleCron, - savedAgent, - libraryAgent, - availableBlocks, - availableFlows, - getOutputType, - saveAgent, - saveAndRun, - stopRun, - createRunSchedule, - isSaving, - isRunning, - isStopping, - isScheduling, - graphExecutionError, - nodes, - setNodes, - edges, - setEdges, - } = useAgentGraph( - flowID, - flowVersion, - flowExecutionID, - visualizeBeads !== "no", - ); - const [immediateNodePositions, setImmediateNodePositions] = useState< - Record - >(Object.fromEntries(nodes.map((node) => [node.id, node.position]))); - - // Add realtime execution status tracking for FloatingReviewsPanel - useFlowRealtime(); - - const router = useRouter(); - const pathname = usePathname(); - const params = useSearchParams(); - const initialPositionRef = useRef<{ - [key: string]: { x: number; y: number }; - }>({}); - const isDragging = useRef(false); - - const runnerUIRef = useRef(null); - - const { toast } = useToast(); - - // It stores the dimension of all nodes with position as well - const [nodeDimensions, setNodeDimensions] = useState({}); - - // Resolution mode state for sub-agent incompatible updates - const [resolutionMode, setResolutionMode] = useState({ - active: false, - nodeId: null, - incompatibilities: null, - brokenEdgeIds: [], - pendingUpdate: null, - }); - - const enterResolutionMode = useCallback( - ( - nodeId: string, - incompatibilities: IncompatibilityInfo, - brokenEdgeIds: string[], - pendingUpdate: Record, - ) => { - setResolutionMode({ - active: true, - nodeId, - incompatibilities, - brokenEdgeIds, - pendingUpdate, - }); - }, - [], - ); - - const exitResolutionMode = useCallback(() => { - setResolutionMode({ - active: false, - nodeId: null, - incompatibilities: null, - brokenEdgeIds: [], - pendingUpdate: null, - }); - }, []); - - // Apply pending update after resolution mode completes - const applyPendingUpdate = useCallback(() => { - if (!resolutionMode.nodeId || !resolutionMode.pendingUpdate) return; - - const node = nodes.find((n) => n.id === resolutionMode.nodeId); - if (node) { - const pendingUpdate = resolutionMode.pendingUpdate as { - [key: string]: any; - }; - setNodes((nds) => - nds.map((n) => - n.id === resolutionMode.nodeId - ? { ...n, data: { ...n.data, hardcodedValues: pendingUpdate } } - : n, - ), - ); - } - exitResolutionMode(); - toast({ - title: "Update complete", - description: "Agent has been updated to the new version.", - }); - }, [resolutionMode, nodes, setNodes, exitResolutionMode, toast]); - - // Check if all broken edges have been removed and auto-apply pending update - useEffect(() => { - if (!resolutionMode.active || resolutionMode.brokenEdgeIds.length === 0) { - return; - } - - const currentEdgeIds = new Set(edges.map((e) => e.id)); - const remainingBrokenEdges = resolutionMode.brokenEdgeIds.filter((id) => - currentEdgeIds.has(id), - ); - - if (remainingBrokenEdges.length === 0) { - // All broken edges have been removed, apply pending update - applyPendingUpdate(); - } else if ( - remainingBrokenEdges.length !== resolutionMode.brokenEdgeIds.length - ) { - // Update the list of broken edges - setResolutionMode((prev) => ({ - ...prev, - brokenEdgeIds: remainingBrokenEdges, - })); - } - }, [edges, resolutionMode, applyPendingUpdate]); - - // Set page title with or without graph name - useEffect(() => { - document.title = savedAgent - ? `${savedAgent.name} - Builder - AutoGPT Platform` - : `Builder - AutoGPT Platform`; - }, [savedAgent]); - - const graphHasWebhookNodes = useMemo( - () => - nodes.some((n) => - [BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes( - n.data.uiType, - ), - ), - [nodes], - ); - - useEffect(() => { - if (params.get("resetTutorial") === "true") { - storage.clean(Key.SHEPHERD_TOUR); - router.push(pathname); - } else if (!storage.get(Key.SHEPHERD_TOUR)) { - const emptyNodes = (forceRemove: boolean = false) => - forceRemove ? (setNodes([]), setEdges([]), true) : nodes.length === 0; - startTutorial(emptyNodes, setPinBlocksPopover, setPinSavePopover); - storage.set(Key.SHEPHERD_TOUR, "yes"); - } - }, [router, pathname, params, setEdges, setNodes, nodes.length]); - - useEffect(() => { - if (params.get("open_scheduling") === "true") { - runnerUIRef.current?.openRunInputDialog(); - } - setFlowExecutionID( - (params.get("flowExecutionID") as GraphExecutionID) || undefined, - ); - }, [params]); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; - const isUndo = - (isMac ? event.metaKey : event.ctrlKey) && event.key === "z"; - const isRedo = - (isMac ? event.metaKey : event.ctrlKey) && - (event.key === "y" || (event.shiftKey && event.key === "Z")); - - if (isUndo) { - event.preventDefault(); - history.undo(); - } - - if (isRedo) { - event.preventDefault(); - history.redo(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, []); - - const onNodeDragStart = (_: MouseEvent, node: Node) => { - initialPositionRef.current[node.id] = { ...node.position }; - isDragging.current = true; - }; - - const onNodeDragEnd = (_: MouseEvent, node: Node | null) => { - if (!node) return; - - isDragging.current = false; - const oldPosition = initialPositionRef.current[node.id]; - const newPosition = node.position; - - // Clear immediate position, because on drag end it is no longer needed - setImmediateNodePositions((prevPositions) => { - const updatedPositions = { ...prevPositions }; - delete updatedPositions[node.id]; - return updatedPositions; - }); - - // Calculate the movement distance - if (!oldPosition || !newPosition) return; - - const distanceMoved = Math.sqrt( - Math.pow(newPosition.x - oldPosition.x, 2) + - Math.pow(newPosition.y - oldPosition.y, 2), - ); - - if (distanceMoved > MINIMUM_MOVE_BEFORE_LOG) { - // Minimum movement threshold - history.push({ - type: "UPDATE_NODE_POSITION", - payload: { nodeId: node.id, oldPosition, newPosition }, - undo: () => updateNode(node.id, { position: oldPosition }), - redo: () => updateNode(node.id, { position: newPosition }), - }); - } - delete initialPositionRef.current[node.id]; - }; - - // Function to clear status, output, and close the output info dropdown of all nodes - // and reset data beads on edges - const clearNodesStatusAndOutput = useCallback(() => { - setNodes((nds) => - nds.map((node) => ({ - ...node, - data: { - ...node.data, - status: undefined, - isOutputOpen: false, - }, - })), - ); - }, [setNodes]); - - const onNodesChange = useCallback( - (nodeChanges: NodeChange[]) => { - // Intercept position changes to update immediate positions & prevent excessive node re-renders - const draggingPosChanges = nodeChanges - .filter((c) => c.type === "position") - .filter((c) => c.dragging === true); - if (draggingPosChanges.length > 0) { - setImmediateNodePositions((prevPositions) => { - const newPositions = { ...prevPositions }; - draggingPosChanges.forEach((change) => { - if (change.position) newPositions[change.id] = change.position; - }); - return newPositions; - }); - - // Don't further process ongoing position changes - nodeChanges = nodeChanges.filter( - (change) => change.type !== "position" || change.dragging !== true, - ); - if (nodeChanges.length === 0) return; - } - - // Persist the changes - setNodes((prev) => applyNodeChanges(nodeChanges, prev)); - - // Remove all edges that were connected to deleted nodes - nodeChanges - .filter((change) => change.type === "remove") - .forEach((deletedNode) => { - const nodeID = deletedNode.id; - const deletedNodeData = nodes.find((node) => node.id === nodeID); - - if (deletedNodeData) { - history.push({ - type: "DELETE_NODE", - payload: { node: deletedNodeData.data }, - undo: () => addNodes(deletedNodeData), - redo: () => deleteElements({ nodes: [{ id: nodeID }] }), - }); - } - - const connectedEdges = edges.filter((edge) => - [edge.source, edge.target].includes(nodeID), - ); - deleteElements({ - edges: connectedEdges.map((edge) => ({ id: edge.id })), - }); - }); - }, - [deleteElements, setNodes, nodes, edges, addNodes], - ); - - const onConnect: OnConnect = useCallback( - (connection: Connection) => { - // Check if this exact connection already exists - const existingConnection = edges.find( - (edge) => - edge.source === connection.source && - edge.target === connection.target && - edge.sourceHandle === connection.sourceHandle && - edge.targetHandle === connection.targetHandle, - ); - - if (existingConnection) { - console.warn("This exact connection already exists."); - return; - } - - const edgeColor = getTypeColor( - getOutputType(nodes, connection.source!, connection.sourceHandle!), - ); - const sourceNode = getNode(connection.source!); - const newEdge: CustomEdge = { - id: formatEdgeID(connection), - type: "custom", - markerEnd: { - type: MarkerType.ArrowClosed, - strokeWidth: 2, - color: edgeColor, - }, - data: { - edgeColor, - sourcePos: sourceNode!.position, - isStatic: sourceNode!.data.isOutputStatic, - beadUp: 0, - beadDown: 0, - }, - ...connection, - source: connection.source!, - target: connection.target!, - }; - - addEdges(newEdge); - history.push({ - type: "ADD_EDGE", - payload: { edge: newEdge }, - undo: () => { - deleteElements({ edges: [{ id: newEdge.id }] }); - }, - redo: () => { - addEdges(newEdge); - }, - }); - clearNodesStatusAndOutput(); // Clear status and output on connection change - }, - [ - getNode, - addEdges, - deleteElements, - clearNodesStatusAndOutput, - nodes, - edges, - formatEdgeID, - getOutputType, - ], - ); - - const onEdgesChange = useCallback( - (edgeChanges: EdgeChange[]) => { - // Persist the changes - setEdges((prev) => applyEdgeChanges(edgeChanges, prev)); - - // Propagate edge changes to node data - const addedEdges = edgeChanges.filter((change) => change.type === "add"), - replaceEdges = edgeChanges.filter( - (change) => change.type === "replace", - ), - removedEdges = edgeChanges.filter((change) => change.type === "remove"); - - if (addedEdges.length > 0 || removedEdges.length > 0) { - setNodes((nds) => { - const newNodes = nds.map((node) => ({ - ...node, - data: { - ...node.data, - connections: [ - // Remove node connections for deleted edges - ...node.data.connections.filter( - (conn) => - !removedEdges.some( - (removedEdge) => removedEdge.id === conn.id, - ), - ), - // Add node connections for added edges - ...addedEdges.map( - (addedEdge): ConnectedEdge => ({ - id: addedEdge.item.id, - source: addedEdge.item.source, - target: addedEdge.item.target, - sourceHandle: addedEdge.item.sourceHandle!, - targetHandle: addedEdge.item.targetHandle!, - }), - ), - ], - }, - })); - - return newNodes; - }); - - if (removedEdges.length > 0) { - clearNodesStatusAndOutput(); // Clear status and output on edge deletion - } - } - - if (replaceEdges.length > 0) { - // Reset node connections for all edges - console.warn( - "useReactFlow().setRootEdges was used to overwrite all edges. " + - "Use addEdges, deleteElements, or reconnectEdge for incremental changes.", - replaceEdges, - ); - setNodes((nds) => - nds.map((node) => ({ - ...node, - data: { - ...node.data, - connections: [ - ...replaceEdges.map( - (replaceEdge): ConnectedEdge => ({ - id: replaceEdge.item.id, - source: replaceEdge.item.source, - target: replaceEdge.item.target, - sourceHandle: replaceEdge.item.sourceHandle!, - targetHandle: replaceEdge.item.targetHandle!, - }), - ), - ], - }, - })), - ); - clearNodesStatusAndOutput(); - } - }, - [setNodes, clearNodesStatusAndOutput, setEdges], - ); - - const getNextNodeId = useCallback(() => { - return uuidv4(); - }, []); - - useEffect(() => { - if (nodes.length === 0) { - return; - } - - if (hasAutoFramed) { - return; - } - - const rafId = requestAnimationFrame(() => { - fitView({ padding: 0.2, duration: 800, maxZoom: 1 }); - setHasAutoFramed(true); - }); - - return () => cancelAnimationFrame(rafId); - }, [fitView, hasAutoFramed, nodes.length]); - - useEffect(() => { - setHasAutoFramed(false); - }, [flowID, flowVersion]); - - const navigateToNode = useCallback( - (nodeId: string) => { - const node = getNode(nodeId); - if (!node) return; - - // Center the viewport on the selected node - const zoom = 1.2; // Slightly zoom in for better visibility - const nodeX = node.position.x + (node.width || 500) / 2; - const nodeY = node.position.y + (node.height || 400) / 2; - - setViewport({ - x: window.innerWidth / 2 - nodeX * zoom, - y: window.innerHeight / 2 - nodeY * zoom, - zoom: zoom, - }); - - // Add a temporary highlight effect to the node - updateNode(nodeId, { - style: { - ...node.style, - boxShadow: "0 0 20px 5px rgba(59, 130, 246, 0.8)", - transition: "box-shadow 0.3s ease-in-out", - }, - }); - - // Remove highlight after a delay - setTimeout(() => { - updateNode(nodeId, { - style: { - ...node.style, - boxShadow: undefined, - }, - }); - }, 2000); - }, - [getNode, setViewport, updateNode], - ); - - const highlightNode = useCallback( - (nodeId: string | null) => { - if (!nodeId) { - // Clear all highlights - nodes.forEach((node) => { - updateNode(node.id, { - style: { - ...node.style, - boxShadow: undefined, - }, - }); - }); - return; - } - - const node = getNode(nodeId); - if (!node) return; - - // Add highlight effect without moving view - updateNode(nodeId, { - style: { - ...node.style, - boxShadow: "0 0 15px 3px rgba(59, 130, 246, 0.6)", - transition: "box-shadow 0.2s ease-in-out", - }, - }); - }, - [getNode, updateNode, nodes], - ); - - /* Shared helper to create and add a node */ - const createAndAddNode = useCallback( - async ( - blockID: string, - blockName: string, - hardcodedValues: Record, - position: { x: number; y: number }, - ): Promise => { - const nodeSchema = availableBlocks.find((node) => node.id === blockID); - if (!nodeSchema) { - console.error(`Schema not found for block ID: ${blockID}`); - return null; - } - - // For agent blocks, fetch the full graph to get schemas - let inputSchema: BlockIORootSchema = nodeSchema.inputSchema; - let outputSchema: BlockIORootSchema = nodeSchema.outputSchema; - let finalHardcodedValues = hardcodedValues; - - if (blockID === SpecialBlockID.AGENT) { - const graphID = hardcodedValues.graph_id as string; - const graphVersion = hardcodedValues.graph_version as number; - const graphData = okData( - await getV1GetSpecificGraph(graphID, { version: graphVersion }), - ); - - if (graphData) { - inputSchema = graphData.input_schema as BlockIORootSchema; - outputSchema = graphData.output_schema as BlockIORootSchema; - finalHardcodedValues = { - ...hardcodedValues, - input_schema: graphData.input_schema, - output_schema: graphData.output_schema, - }; - } else { - console.error("Failed to fetch graph data for agent block"); - } - } - - const newNode: CustomNode = { - id: nodeId.toString(), - type: "custom", - position, - data: { - blockType: blockName, - blockCosts: nodeSchema.costs || [], - title: `${blockName} ${nodeId}`, - description: nodeSchema.description, - categories: nodeSchema.categories, - inputSchema: inputSchema, - outputSchema: outputSchema, - hardcodedValues: finalHardcodedValues, - connections: [], - isOutputOpen: false, - block_id: blockID, - isOutputStatic: nodeSchema.staticOutput, - uiType: nodeSchema.uiType, - }, - }; - - addNodes(newNode); - setNodeId((prevId) => prevId + 1); - clearNodesStatusAndOutput(); - - history.push({ - type: "ADD_NODE", - payload: { node: { ...newNode, ...newNode.data } }, - undo: () => deleteElements({ nodes: [{ id: newNode.id }] }), - redo: () => addNodes(newNode), - }); - - return newNode; - }, - [ - availableBlocks, - nodeId, - addNodes, - deleteElements, - clearNodesStatusAndOutput, - ], - ); - - const addNode = useCallback( - async ( - blockId: string, - nodeType: string, - hardcodedValues: Record = {}, - ) => { - const nodeSchema = availableBlocks.find((node) => node.id === blockId); - if (!nodeSchema) { - console.error(`Schema not found for block ID: ${blockId}`); - return; - } - - /* - Calculate a position to the right of the newly added block, allowing for some margin. - If adding to the right side causes the new block to collide with an existing block, attempt to place it at the bottom or left. - Why not the top? Because the height of the new block is unknown. - If it still collides, run a loop to find the best position where it does not collide. - Then, adjust the canvas to center on the newly added block. - Note: The width is known, e.g., w = 300px for a note and w = 500px for others, but the height is dynamic. - */ - - // Alternative: We could also use D3 force, Intersection for this (React flow Pro examples) - - const { x, y } = getViewport(); - const position = - nodeDimensions && Object.keys(nodeDimensions).length > 0 - ? findNewlyAddedBlockCoordinates( - nodeDimensions, - nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500, - 60, - 1.0, - ) - : { - x: window.innerWidth / 2 - x, - y: window.innerHeight / 2 - y, - }; - - const newNode = await createAndAddNode( - blockId, - nodeType, - hardcodedValues, - position, - ); - if (!newNode) return; - - setViewport( - { - x: -position.x * 0.8 + (window.innerWidth - 0.0) / 2, - y: -position.y * 0.8 + (window.innerHeight - 400) / 2, - zoom: 0.8, - }, - { duration: 500 }, - ); - }, - [ - getViewport, - setViewport, - availableBlocks, - nodeDimensions, - createAndAddNode, - ], - ); - - const findNodeDimensions = useCallback(() => { - const newNodeDimensions: NodeDimension = nodes.reduce((acc, node) => { - const nodeElement = document.querySelector( - `[data-id="custom-node-${node.id}"]`, - ); - if (nodeElement) { - const rect = nodeElement.getBoundingClientRect(); - const { left, top, width, height } = rect; - - const { x, y, zoom } = getViewport(); - - // Convert screen coordinates to flow coordinates - const flowX = (left - x) / zoom; - const flowY = (top - y) / zoom; - const flowWidth = width / zoom; - const flowHeight = height / zoom; - - acc[node.id] = { - x: flowX, - y: flowY, - width: flowWidth, - height: flowHeight, - }; - } - return acc; - }, {} as NodeDimension); - - setNodeDimensions(newNodeDimensions); - }, [nodes, getViewport]); - - useEffect(() => { - findNodeDimensions(); - }, [nodes, findNodeDimensions]); - - const getNodeTitle = useCallback( - (nodeID: string) => { - const node = nodes.find((n) => n.data.backend_id === nodeID); - if (!node) return null; - - return ( - node.data.metadata?.customized_name || - (node.data.uiType == BlockUIType.AGENT && - node.data.hardcodedValues.agent_name) || - node.data.blockType.replace(/Block$/, "") - ); - }, - [nodes], - ); - - const handleCopyPaste = useCopyPaste(getNextNodeId); - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - // Prevent copy/paste if any modal is open or if the focus is on an input element - const activeElement = document.activeElement; - const isInputField = - activeElement?.tagName === "INPUT" || - activeElement?.tagName === "TEXTAREA" || - activeElement?.getAttribute("contenteditable") === "true"; - - if (isAnyModalOpen || isInputField) return; - - handleCopyPaste(event); - }, - [isAnyModalOpen, handleCopyPaste], - ); - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [handleKeyDown]); - - const onNodesDelete = useCallback(() => { - clearNodesStatusAndOutput(); - }, [clearNodesStatusAndOutput]); - - const editorControls: Control[] = useMemo( - () => [ - { - label: "Undo", - icon: , - onClick: history.undo, - }, - { - label: "Redo", - icon: , - onClick: history.redo, - }, - ], - [], - ); - - // Track when we should run or schedule after save completes - const [shouldRunAfterSave, setShouldRunAfterSave] = useState(false); - const [shouldScheduleAfterSave, setShouldScheduleAfterSave] = useState(false); - - // Effect to trigger runOrOpenInput or openRunInputDialog after saving completes - useEffect(() => { - if (!isSaving && shouldRunAfterSave) { - runnerUIRef.current?.runOrOpenInput(); - setShouldRunAfterSave(false); - } - if (!isSaving && shouldScheduleAfterSave) { - runnerUIRef.current?.openRunInputDialog(); - setShouldScheduleAfterSave(false); - } - }, [isSaving, shouldRunAfterSave, shouldScheduleAfterSave]); - - const handleRunButton = useCallback(async () => { - if (isRunning) return; - if (!savedAgent) { - toast({ - title: `Please save the agent first, using the button in the left sidebar.`, - }); - return; - } - await saveAgent(); - setShouldRunAfterSave(true); - }, [isRunning, savedAgent, toast, saveAgent]); - - const handleScheduleButton = useCallback(async () => { - if (isScheduling) return; - if (!savedAgent) { - toast({ - title: `Please save the agent first, using the button in the left sidebar.`, - }); - return; - } - await saveAgent(); - setShouldScheduleAfterSave(true); - }, [isScheduling, savedAgent, toast, saveAgent]); - - const isNewBlockEnabled = useGetFlag(Flag.NEW_BLOCK_MENU); - const isGraphSearchEnabled = useGetFlag(Flag.GRAPH_SEARCH); - - const onDragOver = useCallback((event: React.DragEvent) => { - event.preventDefault(); - event.dataTransfer.dropEffect = "copy"; - }, []); - - const onDrop = useCallback( - async (event: React.DragEvent) => { - event.preventDefault(); - - const blockData = event.dataTransfer.getData("application/reactflow"); - if (!blockData) return; - - try { - const { blockId, blockName, hardcodedValues } = JSON.parse(blockData); - - // Convert screen coordinates to flow coordinates - const position = screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - - await createAndAddNode( - blockId, - blockName, - hardcodedValues || {}, - position, - ); - } catch (error) { - console.error("Failed to drop block:", error); - } - }, - [screenToFlowPosition, createAndAddNode], - ); - - const buildContextValue: BuilderContextType = useMemo( - () => ({ - libraryAgent, - visualizeBeads, - setIsAnyModalOpen, - getNextNodeId, - getNodeTitle, - availableFlows, - resolutionMode, - enterResolutionMode, - exitResolutionMode, - applyPendingUpdate, - }), - [ - libraryAgent, - visualizeBeads, - getNextNodeId, - getNodeTitle, - availableFlows, - resolutionMode, - enterResolutionMode, - applyPendingUpdate, - exitResolutionMode, - ], - ); - - return ( - -
- - node.id in immediateNodePositions - ? { - ...node, - position: immediateNodePositions[node.id] || node.position, - } - : node, - )} - edges={edges} - nodeTypes={{ custom: CustomNode }} - edgeTypes={{ custom: CustomEdge }} - connectionLineComponent={ConnectionLine} - onConnect={onConnect} - onNodesChange={onNodesChange} - onNodesDelete={onNodesDelete} - onEdgesChange={onEdgesChange} - onNodeDragStop={onNodeDragEnd} - onNodeDragStart={onNodeDragStart} - onDrop={onDrop} - onDragOver={onDragOver} - deleteKeyCode={["Backspace", "Delete"]} - minZoom={0.1} - maxZoom={2} - className="dark:bg-slate-900" - > - - - {savedAgent && ( - - )} - {isNewBlockEnabled ? ( - - ) : ( - - - {isGraphSearchEnabled && ( - - )} - - } - botChildren={ - - } - /> - )} - - {!graphHasWebhookNodes ? ( - - ) : ( - - You are building a Trigger Agent - - Your agent{" "} - {savedAgent?.nodes.some((node) => node.webhook) - ? "is listening" - : "will listen"}{" "} - for its trigger and will run when the time is right. -
- You can view its activity in your{" "} - - Agent Library - - . -
-
- )} -
-
- {savedAgent && ( - - )} - - - - -
- ); -}; - -const WrappedFlowEditor: typeof FlowEditor = (props) => ( - - - -); - -export default WrappedFlowEditor; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/flow.css b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/flow.css deleted file mode 100644 index 9786daaff9..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/flow.css +++ /dev/null @@ -1,103 +0,0 @@ -/* flow.css or index.css */ - -body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", - "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -} - -code { - font-family: - source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; -} - -.modal { - position: absolute; - top: 50%; - left: 50%; - right: auto; - bottom: auto; - margin-right: -50%; - transform: translate(-50%, -50%); - background: #ffffff; - padding: 20px; - border: 1px solid #ccc; - border-radius: 4px; - color: #000000; -} - -.overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.75); -} - -.modal h2 { - margin-top: 0; -} - -.modal button { - margin-right: 10px; -} - -.modal form { - display: flex; - flex-direction: column; -} - -.modal form div { - margin-bottom: 15px; -} - -.sidebar { - position: fixed; - top: 0; - left: -600px; - width: 350px; - height: calc(100vh - 68px); /* Full height minus top offset */ - background-color: #ffffff; - color: #000000; - padding: 20px; - transition: left 0.3s ease; - z-index: 1000; - overflow-y: auto; - margin-top: 68px; /* Margin to push content below the top fixed area */ -} - -.sidebar.open { - left: 0; -} - -.sidebar h3 { - margin: 0 0 10px; -} - -.sidebar input { - margin: 0 0 10px; -} - -.sidebarNodeRowStyle { - display: flex; - justify-content: space-between; - align-items: center; - background-color: #e2e2e2; - padding: 10px; - margin-bottom: 10px; - border-radius: 10px; - cursor: grab; -} - -.sidebarNodeRowStyle.dragging { - opacity: 0.5; -} - -.flow-container { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/GraphSearchControl.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/GraphSearchControl.tsx deleted file mode 100644 index 804434372f..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/GraphSearchControl.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/__legacy__/ui/popover"; -import { Button } from "@/components/atoms/Button/Button"; -import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; -import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode"; -import { GraphSearchContent } from "../NewControlPanel/NewSearchGraph/GraphMenuContent/GraphContent"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/atoms/Tooltip/BaseTooltip"; -import { useGraphMenu } from "../NewControlPanel/NewSearchGraph/GraphMenu/useGraphMenu"; - -interface GraphSearchControlProps { - nodes: CustomNode[]; - onNodeSelect: (nodeId: string) => void; - onNodeHover?: (nodeId: string | null) => void; -} - -export function GraphSearchControl({ - nodes, - onNodeSelect, - onNodeHover, -}: GraphSearchControlProps) { - // Use the same hook as GraphSearchMenu for consistency - const { - open, - searchQuery, - setSearchQuery, - filteredNodes, - handleNodeSelect, - handleOpenChange, - } = useGraphMenu({ - nodes, - blockMenuSelected: "", // We don't need to track this in the old control panel - setBlockMenuSelected: () => {}, // Not needed in this context - onNodeSelect, - }); - - return ( - - - - - - - - Search Graph - - - - - - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/InputModalComponent.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/InputModalComponent.tsx deleted file mode 100644 index 2af9ab332e..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/InputModalComponent.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { FC, useEffect, useState } from "react"; -import { Button } from "../../../../../components/__legacy__/ui/button"; -import { Textarea } from "../../../../../components/__legacy__/ui/textarea"; -import { Maximize2, Minimize2, Clipboard } from "lucide-react"; -import { createPortal } from "react-dom"; -import { toast } from "../../../../../components/molecules/Toast/use-toast"; - -interface ModalProps { - isOpen: boolean; - onClose: () => void; - onSave: (value: string) => void; - title?: string; - defaultValue: string; -} - -const InputModalComponent: FC = ({ - isOpen, - onClose, - onSave, - title, - defaultValue, -}) => { - const [tempValue, setTempValue] = useState(defaultValue); - const [isMaximized, setIsMaximized] = useState(false); - - useEffect(() => { - if (isOpen) { - setTempValue(defaultValue); - setIsMaximized(false); - } - }, [isOpen, defaultValue]); - - const handleSave = () => { - onSave(tempValue); - onClose(); - }; - - const toggleSize = () => { - setIsMaximized(!isMaximized); - }; - - const copyValue = () => { - navigator.clipboard.writeText(tempValue).then(() => { - toast({ - title: "Input value copied to clipboard!", - duration: 2000, - }); - }); - }; - - if (!isOpen) { - return null; - } - - const modalContent = ( -