mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-14 09:38:00 -05:00
Compare commits
2 Commits
claude-cod
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b09a94e3f | ||
|
|
61efee4139 |
@@ -81,7 +81,6 @@ export const RunInputDialog = ({
|
|||||||
Inputs
|
Inputs
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2">
|
|
||||||
<FormRenderer
|
<FormRenderer
|
||||||
jsonSchema={inputSchema as RJSFSchema}
|
jsonSchema={inputSchema as RJSFSchema}
|
||||||
handleChange={(v) => handleInputChange(v.formData)}
|
handleChange={(v) => handleInputChange(v.formData)}
|
||||||
@@ -93,7 +92,6 @@ export const RunInputDialog = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/def
|
|||||||
import {
|
import {
|
||||||
useGetV1GetExecutionDetails,
|
useGetV1GetExecutionDetails,
|
||||||
useGetV1GetSpecificGraph,
|
useGetV1GetSpecificGraph,
|
||||||
|
useGetV1ListUserGraphs,
|
||||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||||
@@ -17,6 +18,7 @@ import { useReactFlow } from "@xyflow/react";
|
|||||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||||
import { useHistoryStore } from "../../../stores/historyStore";
|
import { useHistoryStore } from "../../../stores/historyStore";
|
||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
|
import { okData } from "@/app/api/helpers";
|
||||||
|
|
||||||
export const useFlow = () => {
|
export const useFlow = () => {
|
||||||
const [isLocked, setIsLocked] = useState(false);
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
@@ -36,6 +38,9 @@ export const useFlow = () => {
|
|||||||
const setGraphExecutionStatus = useGraphStore(
|
const setGraphExecutionStatus = useGraphStore(
|
||||||
useShallow((state) => state.setGraphExecutionStatus),
|
useShallow((state) => state.setGraphExecutionStatus),
|
||||||
);
|
);
|
||||||
|
const setAvailableSubGraphs = useGraphStore(
|
||||||
|
useShallow((state) => state.setAvailableSubGraphs),
|
||||||
|
);
|
||||||
const updateEdgeBeads = useEdgeStore(
|
const updateEdgeBeads = useEdgeStore(
|
||||||
useShallow((state) => state.updateEdgeBeads),
|
useShallow((state) => state.updateEdgeBeads),
|
||||||
);
|
);
|
||||||
@@ -62,6 +67,11 @@ export const useFlow = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fetch all available graphs for sub-agent update detection
|
||||||
|
const { data: availableGraphs } = useGetV1ListUserGraphs({
|
||||||
|
query: { select: okData },
|
||||||
|
});
|
||||||
|
|
||||||
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
|
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
|
||||||
flowID ?? "",
|
flowID ?? "",
|
||||||
flowVersion !== null ? { version: flowVersion } : {},
|
flowVersion !== null ? { version: flowVersion } : {},
|
||||||
@@ -116,10 +126,18 @@ export const useFlow = () => {
|
|||||||
}
|
}
|
||||||
}, [graph]);
|
}, [graph]);
|
||||||
|
|
||||||
|
// Update available sub-graphs in store for sub-agent update detection
|
||||||
|
useEffect(() => {
|
||||||
|
if (availableGraphs) {
|
||||||
|
setAvailableSubGraphs(availableGraphs);
|
||||||
|
}
|
||||||
|
}, [availableGraphs, setAvailableSubGraphs]);
|
||||||
|
|
||||||
// adding nodes
|
// adding nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customNodes.length > 0) {
|
if (customNodes.length > 0) {
|
||||||
useNodeStore.getState().setNodes([]);
|
useNodeStore.getState().setNodes([]);
|
||||||
|
useNodeStore.getState().clearResolutionState();
|
||||||
addNodes(customNodes);
|
addNodes(customNodes);
|
||||||
|
|
||||||
// Sync hardcoded values with handle IDs.
|
// Sync hardcoded values with handle IDs.
|
||||||
@@ -203,6 +221,7 @@ export const useFlow = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
useNodeStore.getState().setNodes([]);
|
useNodeStore.getState().setNodes([]);
|
||||||
|
useNodeStore.getState().clearResolutionState();
|
||||||
useEdgeStore.getState().setEdges([]);
|
useEdgeStore.getState().setEdges([]);
|
||||||
useGraphStore.getState().reset();
|
useGraphStore.getState().reset();
|
||||||
useEdgeStore.getState().resetEdgeBeads();
|
useEdgeStore.getState().resetEdgeBeads();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getBezierPath,
|
getBezierPath,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
import { XIcon } from "@phosphor-icons/react";
|
import { XIcon } from "@phosphor-icons/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
|
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
|
||||||
@@ -35,6 +36,8 @@ const CustomEdge = ({
|
|||||||
selected,
|
selected,
|
||||||
}: EdgeProps<CustomEdge>) => {
|
}: EdgeProps<CustomEdge>) => {
|
||||||
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
||||||
|
// Subscribe to the brokenEdgeIDs map and check if this edge is broken across any node
|
||||||
|
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getBezierPath({
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
@@ -50,6 +53,12 @@ const CustomEdge = ({
|
|||||||
const beadUp = data?.beadUp ?? 0;
|
const beadUp = data?.beadUp ?? 0;
|
||||||
const beadDown = data?.beadDown ?? 0;
|
const beadDown = data?.beadDown ?? 0;
|
||||||
|
|
||||||
|
const handleRemoveEdge = () => {
|
||||||
|
removeConnection(id);
|
||||||
|
// Note: broken edge tracking is cleaned up automatically by useSubAgentUpdateState
|
||||||
|
// when it detects the edge no longer exists
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
@@ -57,7 +66,9 @@ const CustomEdge = ({
|
|||||||
markerEnd={markerEnd}
|
markerEnd={markerEnd}
|
||||||
className={cn(
|
className={cn(
|
||||||
isStatic && "!stroke-[1.5px] [stroke-dasharray:6]",
|
isStatic && "!stroke-[1.5px] [stroke-dasharray:6]",
|
||||||
selected
|
isBroken
|
||||||
|
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
||||||
|
: selected
|
||||||
? "stroke-zinc-800"
|
? "stroke-zinc-800"
|
||||||
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
||||||
)}
|
)}
|
||||||
@@ -70,12 +81,16 @@ const CustomEdge = ({
|
|||||||
/>
|
/>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => removeConnection(id)}
|
onClick={handleRemoveEdge}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-fit min-w-0 p-1 transition-opacity",
|
"absolute h-fit min-w-0 p-1 transition-opacity",
|
||||||
isHovered ? "opacity-100" : "opacity-0",
|
isBroken
|
||||||
|
? "bg-red-500 opacity-100 hover:bg-red-600"
|
||||||
|
: isHovered
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
variant="secondary"
|
variant={isBroken ? "primary" : "secondary"}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Handle, Position } from "@xyflow/react";
|
|||||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useNodeStore } from "../../../stores/nodeStore";
|
||||||
|
|
||||||
const InputNodeHandle = ({
|
const InputNodeHandle = ({
|
||||||
handleId,
|
handleId,
|
||||||
@@ -15,6 +16,9 @@ const InputNodeHandle = ({
|
|||||||
const isInputConnected = useEdgeStore((state) =>
|
const isInputConnected = useEdgeStore((state) =>
|
||||||
state.isInputConnected(nodeId ?? "", cleanedHandleId),
|
state.isInputConnected(nodeId ?? "", cleanedHandleId),
|
||||||
);
|
);
|
||||||
|
const isInputBroken = useNodeStore((state) =>
|
||||||
|
state.isInputBroken(nodeId, cleanedHandleId),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Handle
|
<Handle
|
||||||
@@ -27,7 +31,10 @@ const InputNodeHandle = ({
|
|||||||
<CircleIcon
|
<CircleIcon
|
||||||
size={16}
|
size={16}
|
||||||
weight={isInputConnected ? "fill" : "duotone"}
|
weight={isInputConnected ? "fill" : "duotone"}
|
||||||
className={"text-gray-400 opacity-100"}
|
className={cn(
|
||||||
|
"text-gray-400 opacity-100",
|
||||||
|
isInputBroken && "text-red-500",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
@@ -38,14 +45,17 @@ const OutputNodeHandle = ({
|
|||||||
field_name,
|
field_name,
|
||||||
nodeId,
|
nodeId,
|
||||||
hexColor,
|
hexColor,
|
||||||
|
isBroken,
|
||||||
}: {
|
}: {
|
||||||
field_name: string;
|
field_name: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
hexColor: string;
|
hexColor: string;
|
||||||
|
isBroken: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const isOutputConnected = useEdgeStore((state) =>
|
const isOutputConnected = useEdgeStore((state) =>
|
||||||
state.isOutputConnected(nodeId, field_name),
|
state.isOutputConnected(nodeId, field_name),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Handle
|
<Handle
|
||||||
type={"source"}
|
type={"source"}
|
||||||
@@ -58,7 +68,10 @@ const OutputNodeHandle = ({
|
|||||||
size={16}
|
size={16}
|
||||||
weight={"duotone"}
|
weight={"duotone"}
|
||||||
color={isOutputConnected ? hexColor : "gray"}
|
color={isOutputConnected ? hexColor : "gray"}
|
||||||
className={cn("text-gray-400 opacity-100")}
|
className={cn(
|
||||||
|
"text-gray-400 opacity-100",
|
||||||
|
isBroken && "text-red-500",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
|||||||
import { NodeRightClickMenu } from "./components/NodeRightClickMenu";
|
import { NodeRightClickMenu } from "./components/NodeRightClickMenu";
|
||||||
import { StickyNoteBlock } from "./components/StickyNoteBlock";
|
import { StickyNoteBlock } from "./components/StickyNoteBlock";
|
||||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||||
|
import { SubAgentUpdateFeature } from "./components/SubAgentUpdate/SubAgentUpdateFeature";
|
||||||
|
import { useCustomNode } from "./useCustomNode";
|
||||||
|
|
||||||
export type CustomNodeData = {
|
export type CustomNodeData = {
|
||||||
hardcodedValues: {
|
hardcodedValues: {
|
||||||
@@ -45,6 +47,10 @@ export type CustomNode = XYNode<CustomNodeData, "custom">;
|
|||||||
|
|
||||||
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||||
({ data, id: nodeId, selected }) => {
|
({ data, id: nodeId, selected }) => {
|
||||||
|
const { inputSchema, outputSchema } = useCustomNode({ data, nodeId });
|
||||||
|
|
||||||
|
const isAgent = data.uiType === BlockUIType.AGENT;
|
||||||
|
|
||||||
if (data.uiType === BlockUIType.NOTE) {
|
if (data.uiType === BlockUIType.NOTE) {
|
||||||
return (
|
return (
|
||||||
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
|
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
|
||||||
@@ -63,16 +69,6 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
|||||||
|
|
||||||
const isAyrshare = data.uiType === BlockUIType.AYRSHARE;
|
const isAyrshare = data.uiType === BlockUIType.AYRSHARE;
|
||||||
|
|
||||||
const inputSchema =
|
|
||||||
data.uiType === BlockUIType.AGENT
|
|
||||||
? (data.hardcodedValues.input_schema ?? {})
|
|
||||||
: data.inputSchema;
|
|
||||||
|
|
||||||
const outputSchema =
|
|
||||||
data.uiType === BlockUIType.AGENT
|
|
||||||
? (data.hardcodedValues.output_schema ?? {})
|
|
||||||
: data.outputSchema;
|
|
||||||
|
|
||||||
const hasConfigErrors =
|
const hasConfigErrors =
|
||||||
data.errors &&
|
data.errors &&
|
||||||
Object.values(data.errors).some(
|
Object.values(data.errors).some(
|
||||||
@@ -87,12 +83,11 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
|||||||
|
|
||||||
const hasErrors = hasConfigErrors || hasOutputError;
|
const hasErrors = hasConfigErrors || hasOutputError;
|
||||||
|
|
||||||
// Currently all blockTypes design are similar - that's why i am using the same component for all of them
|
|
||||||
// If in future - if we need some drastic change in some blockTypes design - we can create separate components for them
|
|
||||||
const node = (
|
const node = (
|
||||||
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
|
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
|
||||||
<div className="rounded-xlarge bg-white">
|
<div className="rounded-xlarge bg-white">
|
||||||
<NodeHeader data={data} nodeId={nodeId} />
|
<NodeHeader data={data} nodeId={nodeId} />
|
||||||
|
{isAgent && <SubAgentUpdateFeature nodeID={nodeId} nodeData={data} />}
|
||||||
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
|
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
|
||||||
{isAyrshare && <AyrshareConnectButton />}
|
{isAyrshare && <AyrshareConnectButton />}
|
||||||
<FormCreator
|
<FormCreator
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ArrowUpIcon, WarningIcon } from "@phosphor-icons/react";
|
||||||
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
|
import { cn, beautifyString } from "@/lib/utils";
|
||||||
|
import { CustomNodeData } from "../../CustomNode";
|
||||||
|
import { useSubAgentUpdateState } from "./useSubAgentUpdateState";
|
||||||
|
import { IncompatibleUpdateDialog } from "./components/IncompatibleUpdateDialog";
|
||||||
|
import { ResolutionModeBar } from "./components/ResolutionModeBar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline component for the update bar that can be placed after the header.
|
||||||
|
* Use this inside the node content where you want the bar to appear.
|
||||||
|
*/
|
||||||
|
type SubAgentUpdateFeatureProps = {
|
||||||
|
nodeID: string;
|
||||||
|
nodeData: CustomNodeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SubAgentUpdateFeature({
|
||||||
|
nodeID,
|
||||||
|
nodeData,
|
||||||
|
}: SubAgentUpdateFeatureProps) {
|
||||||
|
const {
|
||||||
|
updateInfo,
|
||||||
|
isInResolutionMode,
|
||||||
|
handleUpdateClick,
|
||||||
|
showIncompatibilityDialog,
|
||||||
|
setShowIncompatibilityDialog,
|
||||||
|
handleConfirmIncompatibleUpdate,
|
||||||
|
} = useSubAgentUpdateState({ nodeID: nodeID, nodeData: nodeData });
|
||||||
|
|
||||||
|
const agentName = nodeData.title || "Agent";
|
||||||
|
|
||||||
|
if (!updateInfo.hasUpdate && !isInResolutionMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isInResolutionMode ? (
|
||||||
|
<ResolutionModeBar incompatibilities={updateInfo.incompatibilities} />
|
||||||
|
) : (
|
||||||
|
<SubAgentUpdateAvailableBar
|
||||||
|
currentVersion={updateInfo.currentVersion}
|
||||||
|
latestVersion={updateInfo.latestVersion}
|
||||||
|
isCompatible={updateInfo.isCompatible}
|
||||||
|
onUpdate={handleUpdateClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Incompatibility dialog - rendered here since this component owns the state */}
|
||||||
|
{updateInfo.incompatibilities && (
|
||||||
|
<IncompatibleUpdateDialog
|
||||||
|
isOpen={showIncompatibilityDialog}
|
||||||
|
onClose={() => setShowIncompatibilityDialog(false)}
|
||||||
|
onConfirm={handleConfirmIncompatibleUpdate}
|
||||||
|
currentVersion={updateInfo.currentVersion}
|
||||||
|
latestVersion={updateInfo.latestVersion}
|
||||||
|
agentName={beautifyString(agentName)}
|
||||||
|
incompatibilities={updateInfo.incompatibilities}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubAgentUpdateAvailableBarProps = {
|
||||||
|
currentVersion: number;
|
||||||
|
latestVersion: number;
|
||||||
|
isCompatible: boolean;
|
||||||
|
onUpdate: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SubAgentUpdateAvailableBar({
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
isCompatible,
|
||||||
|
onUpdate,
|
||||||
|
}: SubAgentUpdateAvailableBarProps): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-t-xl bg-blue-50 px-3 py-2 dark:bg-blue-900/30">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArrowUpIcon className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
<span className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Update available (v{currentVersion} → v{latestVersion})
|
||||||
|
</span>
|
||||||
|
{!isCompatible && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<WarningIcon className="h-4 w-4 text-amber-500" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<p className="font-medium">Incompatible changes detected</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Click Update to see details
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant={isCompatible ? "primary" : "outline"}
|
||||||
|
onClick={onUpdate}
|
||||||
|
className={cn(
|
||||||
|
"h-7 text-xs",
|
||||||
|
!isCompatible && "border-amber-500 text-amber-600 hover:bg-amber-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
WarningIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
PlusCircleIcon,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
|
||||||
|
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||||
|
import { beautifyString } from "@/lib/utils";
|
||||||
|
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate/types";
|
||||||
|
|
||||||
|
type IncompatibleUpdateDialogProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
currentVersion: number;
|
||||||
|
latestVersion: number;
|
||||||
|
agentName: string;
|
||||||
|
incompatibilities: IncompatibilityInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IncompatibleUpdateDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
agentName,
|
||||||
|
incompatibilities,
|
||||||
|
}: IncompatibleUpdateDialogProps) {
|
||||||
|
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 (
|
||||||
|
<Dialog
|
||||||
|
title={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WarningIcon className="h-5 w-5 text-amber-500" weight="fill" />
|
||||||
|
Incompatible Update
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
controlled={{
|
||||||
|
isOpen,
|
||||||
|
set: async (open) => {
|
||||||
|
if (!open) onClose();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClose={onClose}
|
||||||
|
styling={{ maxWidth: "32rem" }}
|
||||||
|
>
|
||||||
|
<Dialog.Content>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Updating <strong>{beautifyString(agentName)}</strong> from v
|
||||||
|
{currentVersion} to v{latestVersion} will break some connections.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Input changes - two column layout */}
|
||||||
|
{hasInputChanges && (
|
||||||
|
<TwoColumnSection
|
||||||
|
title="Input Changes"
|
||||||
|
leftIcon={
|
||||||
|
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
|
||||||
|
}
|
||||||
|
leftTitle="Removed"
|
||||||
|
leftItems={incompatibilities.missingInputs}
|
||||||
|
rightIcon={
|
||||||
|
<PlusCircleIcon
|
||||||
|
className="h-4 w-4 text-green-500"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightTitle="Added"
|
||||||
|
rightItems={incompatibilities.newInputs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Output changes - two column layout */}
|
||||||
|
{hasOutputChanges && (
|
||||||
|
<TwoColumnSection
|
||||||
|
title="Output Changes"
|
||||||
|
leftIcon={
|
||||||
|
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
|
||||||
|
}
|
||||||
|
leftTitle="Removed"
|
||||||
|
leftItems={incompatibilities.missingOutputs}
|
||||||
|
rightIcon={
|
||||||
|
<PlusCircleIcon
|
||||||
|
className="h-4 w-4 text-green-500"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightTitle="Added"
|
||||||
|
rightItems={incompatibilities.newOutputs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasTypeMismatches && (
|
||||||
|
<SingleColumnSection
|
||||||
|
icon={
|
||||||
|
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
|
||||||
|
}
|
||||||
|
title="Type Changed"
|
||||||
|
description="These connected inputs have a different type:"
|
||||||
|
items={incompatibilities.inputTypeMismatches.map(
|
||||||
|
(m) => `${m.name} (${m.oldType} → ${m.newType})`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasNewRequired && (
|
||||||
|
<SingleColumnSection
|
||||||
|
icon={
|
||||||
|
<PlusCircleIcon
|
||||||
|
className="h-4 w-4 text-amber-500"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="New Required Inputs"
|
||||||
|
description="These inputs are now required:"
|
||||||
|
items={incompatibilities.newRequiredInputs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
If you proceed, you'll need to remove the broken connections
|
||||||
|
before you can save or run your agent.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="ghost" size="small" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={onConfirm}
|
||||||
|
className="border-amber-700 bg-amber-600 hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
Update Anyway
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoColumnSectionProps = {
|
||||||
|
title: string;
|
||||||
|
leftIcon: React.ReactNode;
|
||||||
|
leftTitle: string;
|
||||||
|
leftItems: string[];
|
||||||
|
rightIcon: React.ReactNode;
|
||||||
|
rightTitle: string;
|
||||||
|
rightItems: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function TwoColumnSection({
|
||||||
|
title,
|
||||||
|
leftIcon,
|
||||||
|
leftTitle,
|
||||||
|
leftItems,
|
||||||
|
rightIcon,
|
||||||
|
rightTitle,
|
||||||
|
rightItems,
|
||||||
|
}: TwoColumnSectionProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
<span className="font-medium">{title}</span>
|
||||||
|
<div className="mt-2 grid grid-cols-2 items-start gap-4">
|
||||||
|
{/* Left column - Breaking changes */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{leftIcon}
|
||||||
|
<span>{leftTitle}</span>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-1.5 space-y-1">
|
||||||
|
{leftItems.length > 0 ? (
|
||||||
|
leftItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<code className="rounded bg-red-50 px-1 py-0.5 font-mono text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
|
||||||
|
{item}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
||||||
|
None
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column - Possible solutions */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{rightIcon}
|
||||||
|
<span>{rightTitle}</span>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-1.5 space-y-1">
|
||||||
|
{rightItems.length > 0 ? (
|
||||||
|
rightItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<code className="rounded bg-green-50 px-1 py-0.5 font-mono text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||||
|
{item}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
||||||
|
None
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SingleColumnSectionProps = {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function SingleColumnSection({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
items,
|
||||||
|
}: SingleColumnSectionProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icon}
|
||||||
|
<span className="font-medium">{title}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 space-y-1">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="ml-4 list-disc text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs dark:bg-gray-800">
|
||||||
|
{item}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { InfoIcon, WarningIcon } from "@phosphor-icons/react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
|
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate/types";
|
||||||
|
|
||||||
|
type ResolutionModeBarProps = {
|
||||||
|
incompatibilities: IncompatibilityInfo | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ResolutionModeBar({
|
||||||
|
incompatibilities,
|
||||||
|
}: ResolutionModeBarProps): React.ReactElement {
|
||||||
|
const renderIncompatibilities = () => {
|
||||||
|
if (!incompatibilities) return <span>No incompatibilities</span>;
|
||||||
|
|
||||||
|
const sections: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
if (incompatibilities.missingInputs.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
<div key="missing-inputs" className="mb-1">
|
||||||
|
<span className="font-semibold">Missing inputs: </span>
|
||||||
|
{incompatibilities.missingInputs.map((name, i) => (
|
||||||
|
<React.Fragment key={name}>
|
||||||
|
<code className="font-mono">{name}</code>
|
||||||
|
{i < incompatibilities.missingInputs.length - 1 && ", "}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (incompatibilities.missingOutputs.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
<div key="missing-outputs" className="mb-1">
|
||||||
|
<span className="font-semibold">Missing outputs: </span>
|
||||||
|
{incompatibilities.missingOutputs.map((name, i) => (
|
||||||
|
<React.Fragment key={name}>
|
||||||
|
<code className="font-mono">{name}</code>
|
||||||
|
{i < incompatibilities.missingOutputs.length - 1 && ", "}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (incompatibilities.newRequiredInputs.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
<div key="new-required" className="mb-1">
|
||||||
|
<span className="font-semibold">New required inputs: </span>
|
||||||
|
{incompatibilities.newRequiredInputs.map((name, i) => (
|
||||||
|
<React.Fragment key={name}>
|
||||||
|
<code className="font-mono">{name}</code>
|
||||||
|
{i < incompatibilities.newRequiredInputs.length - 1 && ", "}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (incompatibilities.inputTypeMismatches.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
<div key="type-mismatches" className="mb-1">
|
||||||
|
<span className="font-semibold">Type changed: </span>
|
||||||
|
{incompatibilities.inputTypeMismatches.map((m, i) => (
|
||||||
|
<React.Fragment key={m.name}>
|
||||||
|
<code className="font-mono">{m.name}</code>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{" "}
|
||||||
|
({m.oldType} → {m.newType})
|
||||||
|
</span>
|
||||||
|
{i < incompatibilities.inputTypeMismatches.length - 1 && ", "}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{sections}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-t-xl bg-amber-50 px-3 py-2 dark:bg-amber-900/30">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WarningIcon className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
<span className="text-sm text-amber-700 dark:text-amber-300">
|
||||||
|
Remove incompatible connections
|
||||||
|
</span>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<InfoIcon className="h-4 w-4 cursor-help text-amber-500" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-sm">
|
||||||
|
<p className="mb-2 font-semibold">Incompatible changes:</p>
|
||||||
|
<div className="text-xs">{renderIncompatibilities()}</div>
|
||||||
|
<p className="mt-2 text-xs text-gray-400">
|
||||||
|
{(incompatibilities?.newRequiredInputs.length ?? 0) > 0
|
||||||
|
? "Replace / delete"
|
||||||
|
: "Delete"}{" "}
|
||||||
|
the red connections to continue
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { useState, useCallback, useEffect } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||||
|
import {
|
||||||
|
useNodeStore,
|
||||||
|
NodeResolutionData,
|
||||||
|
} from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
|
import {
|
||||||
|
useSubAgentUpdate,
|
||||||
|
createUpdatedAgentNodeInputs,
|
||||||
|
getBrokenEdgeIDs,
|
||||||
|
} from "@/app/(platform)/build/hooks/useSubAgentUpdate";
|
||||||
|
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
||||||
|
import { CustomNodeData } from "../../CustomNode";
|
||||||
|
|
||||||
|
// Stable empty set to avoid creating new references in selectors
|
||||||
|
const EMPTY_SET: Set<string> = new Set();
|
||||||
|
|
||||||
|
type UseSubAgentUpdateParams = {
|
||||||
|
nodeID: string;
|
||||||
|
nodeData: CustomNodeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSubAgentUpdateState({
|
||||||
|
nodeID,
|
||||||
|
nodeData,
|
||||||
|
}: UseSubAgentUpdateParams) {
|
||||||
|
const [showIncompatibilityDialog, setShowIncompatibilityDialog] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
// Get store actions
|
||||||
|
const updateNodeData = useNodeStore(
|
||||||
|
useShallow((state) => state.updateNodeData),
|
||||||
|
);
|
||||||
|
const setNodeResolutionMode = useNodeStore(
|
||||||
|
useShallow((state) => state.setNodeResolutionMode),
|
||||||
|
);
|
||||||
|
const isNodeInResolutionMode = useNodeStore(
|
||||||
|
useShallow((state) => state.isNodeInResolutionMode),
|
||||||
|
);
|
||||||
|
const setBrokenEdgeIDs = useNodeStore(
|
||||||
|
useShallow((state) => state.setBrokenEdgeIDs),
|
||||||
|
);
|
||||||
|
// Get this node's broken edge IDs from the per-node map
|
||||||
|
// Use EMPTY_SET as fallback to maintain referential stability
|
||||||
|
const brokenEdgeIDs = useNodeStore(
|
||||||
|
(state) => state.brokenEdgeIDs.get(nodeID) || EMPTY_SET,
|
||||||
|
);
|
||||||
|
const getNodeResolutionData = useNodeStore(
|
||||||
|
useShallow((state) => state.getNodeResolutionData),
|
||||||
|
);
|
||||||
|
const connectedEdges = useEdgeStore(
|
||||||
|
useShallow((state) => state.getNodeEdges(nodeID)),
|
||||||
|
);
|
||||||
|
const availableSubGraphs = useGraphStore(
|
||||||
|
useShallow((state) => state.availableSubGraphs),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract agent-specific data
|
||||||
|
const graphID = nodeData.hardcodedValues?.graph_id as string | undefined;
|
||||||
|
const graphVersion = nodeData.hardcodedValues?.graph_version as
|
||||||
|
| number
|
||||||
|
| undefined;
|
||||||
|
const currentInputSchema = nodeData.hardcodedValues?.input_schema as
|
||||||
|
| GraphInputSchema
|
||||||
|
| undefined;
|
||||||
|
const currentOutputSchema = nodeData.hardcodedValues?.output_schema as
|
||||||
|
| GraphOutputSchema
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
// Use the sub-agent update hook
|
||||||
|
const updateInfo = useSubAgentUpdate(
|
||||||
|
nodeID,
|
||||||
|
graphID,
|
||||||
|
graphVersion,
|
||||||
|
currentInputSchema,
|
||||||
|
currentOutputSchema,
|
||||||
|
connectedEdges,
|
||||||
|
availableSubGraphs,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInResolutionMode = isNodeInResolutionMode(nodeID);
|
||||||
|
|
||||||
|
// Handle update button click
|
||||||
|
const handleUpdateClick = useCallback(() => {
|
||||||
|
if (!updateInfo.hasUpdate || !updateInfo.latestGraph) return;
|
||||||
|
|
||||||
|
if (updateInfo.isCompatible) {
|
||||||
|
// Compatible update - apply directly
|
||||||
|
const newHardcodedValues = createUpdatedAgentNodeInputs(
|
||||||
|
nodeData.hardcodedValues,
|
||||||
|
updateInfo.latestGraph,
|
||||||
|
);
|
||||||
|
updateNodeData(nodeID, { hardcodedValues: newHardcodedValues });
|
||||||
|
} else {
|
||||||
|
// Incompatible update - show dialog
|
||||||
|
setShowIncompatibilityDialog(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
updateInfo.hasUpdate,
|
||||||
|
updateInfo.latestGraph,
|
||||||
|
updateInfo.isCompatible,
|
||||||
|
nodeData.hardcodedValues,
|
||||||
|
updateNodeData,
|
||||||
|
nodeID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Handle confirming an incompatible update
|
||||||
|
function handleConfirmIncompatibleUpdate() {
|
||||||
|
if (!updateInfo.latestGraph || !updateInfo.incompatibilities) return;
|
||||||
|
|
||||||
|
const latestGraph = updateInfo.latestGraph;
|
||||||
|
|
||||||
|
// Get the new schemas from the latest graph version
|
||||||
|
const newInputSchema =
|
||||||
|
(latestGraph.input_schema as Record<string, unknown>) || {};
|
||||||
|
const newOutputSchema =
|
||||||
|
(latestGraph.output_schema as Record<string, unknown>) || {};
|
||||||
|
|
||||||
|
// Create the updated hardcoded values but DON'T apply them yet
|
||||||
|
// We'll apply them when resolution is complete
|
||||||
|
const pendingHardcodedValues = createUpdatedAgentNodeInputs(
|
||||||
|
nodeData.hardcodedValues,
|
||||||
|
latestGraph,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get broken edge IDs and store them for this node
|
||||||
|
const brokenIds = getBrokenEdgeIDs(
|
||||||
|
connectedEdges,
|
||||||
|
updateInfo.incompatibilities,
|
||||||
|
nodeID,
|
||||||
|
);
|
||||||
|
setBrokenEdgeIDs(nodeID, brokenIds);
|
||||||
|
|
||||||
|
// Enter resolution mode with both old and new schemas
|
||||||
|
// DON'T apply the update yet - keep old schema so connections remain visible
|
||||||
|
const resolutionData: NodeResolutionData = {
|
||||||
|
incompatibilities: updateInfo.incompatibilities,
|
||||||
|
pendingUpdate: {
|
||||||
|
input_schema: newInputSchema,
|
||||||
|
output_schema: newOutputSchema,
|
||||||
|
},
|
||||||
|
currentSchema: {
|
||||||
|
input_schema: (currentInputSchema as Record<string, unknown>) || {},
|
||||||
|
output_schema: (currentOutputSchema as Record<string, unknown>) || {},
|
||||||
|
},
|
||||||
|
pendingHardcodedValues,
|
||||||
|
};
|
||||||
|
setNodeResolutionMode(nodeID, true, resolutionData);
|
||||||
|
|
||||||
|
setShowIncompatibilityDialog(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if resolution is complete (all broken edges removed)
|
||||||
|
const resolutionData = getNodeResolutionData(nodeID);
|
||||||
|
|
||||||
|
// Auto-check resolution on edge changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInResolutionMode) return;
|
||||||
|
|
||||||
|
// Check if any broken edges still exist
|
||||||
|
const remainingBroken = Array.from(brokenEdgeIDs).filter((edgeId) =>
|
||||||
|
connectedEdges.some((e) => e.id === edgeId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remainingBroken.length === 0) {
|
||||||
|
// Resolution complete - now apply the pending update
|
||||||
|
if (resolutionData?.pendingHardcodedValues) {
|
||||||
|
updateNodeData(nodeID, {
|
||||||
|
hardcodedValues: resolutionData.pendingHardcodedValues,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// setNodeResolutionMode will clean up this node's broken edges automatically
|
||||||
|
setNodeResolutionMode(nodeID, false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isInResolutionMode,
|
||||||
|
brokenEdgeIDs,
|
||||||
|
connectedEdges,
|
||||||
|
resolutionData,
|
||||||
|
nodeID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateInfo,
|
||||||
|
isInResolutionMode,
|
||||||
|
resolutionData,
|
||||||
|
showIncompatibilityDialog,
|
||||||
|
setShowIncompatibilityDialog,
|
||||||
|
handleUpdateClick,
|
||||||
|
handleConfirmIncompatibleUpdate,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
|
import { NodeResolutionData } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
import { RJSFSchema } from "@rjsf/utils";
|
||||||
|
|
||||||
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
||||||
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
||||||
@@ -9,3 +11,48 @@ export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
|||||||
TERMINATED: "ring-orange-300 bg-orange-300 ",
|
TERMINATED: "ring-orange-300 bg-orange-300 ",
|
||||||
FAILED: "ring-red-300 bg-red-300",
|
FAILED: "ring-red-300 bg-red-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges schemas during resolution mode to include removed inputs/outputs
|
||||||
|
* that still have connections, so users can see and delete them.
|
||||||
|
*/
|
||||||
|
export function mergeSchemaForResolution(
|
||||||
|
currentSchema: Record<string, unknown>,
|
||||||
|
newSchema: Record<string, unknown>,
|
||||||
|
resolutionData: NodeResolutionData,
|
||||||
|
type: "input" | "output",
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const newProps = (newSchema.properties as RJSFSchema) || {};
|
||||||
|
const currentProps = (currentSchema.properties as RJSFSchema) || {};
|
||||||
|
const mergedProps = { ...newProps };
|
||||||
|
const incomp = resolutionData.incompatibilities;
|
||||||
|
|
||||||
|
if (type === "input") {
|
||||||
|
// Add back missing inputs that have connections
|
||||||
|
incomp.missingInputs.forEach((inputName: string) => {
|
||||||
|
if (currentProps[inputName]) {
|
||||||
|
mergedProps[inputName] = currentProps[inputName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Add back inputs with type mismatches (keep old type so connection works visually)
|
||||||
|
incomp.inputTypeMismatches.forEach(
|
||||||
|
(mismatch: { name: string; oldType: string; newType: string }) => {
|
||||||
|
if (currentProps[mismatch.name]) {
|
||||||
|
mergedProps[mismatch.name] = currentProps[mismatch.name];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Add back missing outputs that have connections
|
||||||
|
incomp.missingOutputs.forEach((outputName: string) => {
|
||||||
|
if (currentProps[outputName]) {
|
||||||
|
mergedProps[outputName] = currentProps[outputName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...newSchema,
|
||||||
|
properties: mergedProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
import { CustomNodeData } from "./CustomNode";
|
||||||
|
import { BlockUIType } from "../../../types";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { mergeSchemaForResolution } from "./helpers";
|
||||||
|
|
||||||
|
export const useCustomNode = ({
|
||||||
|
data,
|
||||||
|
nodeId,
|
||||||
|
}: {
|
||||||
|
data: CustomNodeData;
|
||||||
|
nodeId: string;
|
||||||
|
}) => {
|
||||||
|
const isInResolutionMode = useNodeStore((state) =>
|
||||||
|
state.nodesInResolutionMode.has(nodeId),
|
||||||
|
);
|
||||||
|
const resolutionData = useNodeStore((state) =>
|
||||||
|
state.nodeResolutionData.get(nodeId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAgent = data.uiType === BlockUIType.AGENT;
|
||||||
|
|
||||||
|
const currentInputSchema = isAgent
|
||||||
|
? (data.hardcodedValues.input_schema ?? {})
|
||||||
|
: data.inputSchema;
|
||||||
|
const currentOutputSchema = isAgent
|
||||||
|
? (data.hardcodedValues.output_schema ?? {})
|
||||||
|
: data.outputSchema;
|
||||||
|
|
||||||
|
const inputSchema = useMemo(() => {
|
||||||
|
if (isAgent && isInResolutionMode && resolutionData) {
|
||||||
|
return mergeSchemaForResolution(
|
||||||
|
resolutionData.currentSchema.input_schema,
|
||||||
|
resolutionData.pendingUpdate.input_schema,
|
||||||
|
resolutionData,
|
||||||
|
"input",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return currentInputSchema;
|
||||||
|
}, [isAgent, isInResolutionMode, resolutionData, currentInputSchema]);
|
||||||
|
|
||||||
|
const outputSchema = useMemo(() => {
|
||||||
|
if (isAgent && isInResolutionMode && resolutionData) {
|
||||||
|
return mergeSchemaForResolution(
|
||||||
|
resolutionData.currentSchema.output_schema,
|
||||||
|
resolutionData.pendingUpdate.output_schema,
|
||||||
|
resolutionData,
|
||||||
|
"output",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return currentOutputSchema;
|
||||||
|
}, [isAgent, isInResolutionMode, resolutionData, currentOutputSchema]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputSchema,
|
||||||
|
outputSchema,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -5,20 +5,16 @@ import { useNodeStore } from "../../../stores/nodeStore";
|
|||||||
import { BlockUIType } from "../../types";
|
import { BlockUIType } from "../../types";
|
||||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||||
|
|
||||||
export const FormCreator = React.memo(
|
interface FormCreatorProps {
|
||||||
({
|
|
||||||
jsonSchema,
|
|
||||||
nodeId,
|
|
||||||
uiType,
|
|
||||||
showHandles = true,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
jsonSchema: RJSFSchema;
|
jsonSchema: RJSFSchema;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
uiType: BlockUIType;
|
uiType: BlockUIType;
|
||||||
showHandles?: boolean;
|
showHandles?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}
|
||||||
|
|
||||||
|
export const FormCreator: React.FC<FormCreatorProps> = React.memo(
|
||||||
|
({ jsonSchema, nodeId, uiType, showHandles = true, className }) => {
|
||||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||||
|
|
||||||
const getHardCodedValues = useNodeStore(
|
const getHardCodedValues = useNodeStore(
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
import { getTypeDisplayInfo } from "./helpers";
|
import { getTypeDisplayInfo } from "./helpers";
|
||||||
import { BlockUIType } from "../../types";
|
import { BlockUIType } from "../../types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useBrokenOutputs } from "./useBrokenOutputs";
|
||||||
|
|
||||||
export const OutputHandler = ({
|
export const OutputHandler = ({
|
||||||
outputSchema,
|
outputSchema,
|
||||||
@@ -27,6 +29,9 @@ export const OutputHandler = ({
|
|||||||
const { isOutputConnected } = useEdgeStore();
|
const { isOutputConnected } = useEdgeStore();
|
||||||
const properties = outputSchema?.properties || {};
|
const properties = outputSchema?.properties || {};
|
||||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||||
|
const brokenOutputs = useBrokenOutputs(nodeId);
|
||||||
|
|
||||||
|
console.log("brokenOutputs", brokenOutputs);
|
||||||
|
|
||||||
const showHandles = uiType !== BlockUIType.OUTPUT;
|
const showHandles = uiType !== BlockUIType.OUTPUT;
|
||||||
|
|
||||||
@@ -44,6 +49,7 @@ export const OutputHandler = ({
|
|||||||
const shouldShow = isConnected || isOutputVisible;
|
const shouldShow = isConnected || isOutputVisible;
|
||||||
const { displayType, colorClass, hexColor } =
|
const { displayType, colorClass, hexColor } =
|
||||||
getTypeDisplayInfo(fieldSchema);
|
getTypeDisplayInfo(fieldSchema);
|
||||||
|
const isBroken = brokenOutputs.has(fullKey);
|
||||||
|
|
||||||
return shouldShow ? (
|
return shouldShow ? (
|
||||||
<div key={fullKey} className="flex flex-col items-end gap-2">
|
<div key={fullKey} className="flex flex-col items-end gap-2">
|
||||||
@@ -64,15 +70,29 @@ export const OutputHandler = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
<Text variant="body" className="text-slate-700">
|
<Text
|
||||||
|
variant="body"
|
||||||
|
className={cn(
|
||||||
|
"text-slate-700",
|
||||||
|
isBroken && "text-red-500 line-through",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{fieldTitle}
|
{fieldTitle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="small" as="span" className={colorClass}>
|
<Text
|
||||||
|
variant="small"
|
||||||
|
as="span"
|
||||||
|
className={cn(
|
||||||
|
colorClass,
|
||||||
|
isBroken && "!text-red-500 line-through",
|
||||||
|
)}
|
||||||
|
>
|
||||||
({displayType})
|
({displayType})
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{showHandles && (
|
{showHandles && (
|
||||||
<OutputNodeHandle
|
<OutputNodeHandle
|
||||||
|
isBroken={isBroken}
|
||||||
field_name={fullKey}
|
field_name={fullKey}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
hexColor={hexColor}
|
hexColor={hexColor}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get the set of broken output names for a node in resolution mode.
|
||||||
|
*/
|
||||||
|
export function useBrokenOutputs(nodeID: string): Set<string> {
|
||||||
|
// Subscribe to the actual state values, not just methods
|
||||||
|
const isInResolution = useNodeStore((state) =>
|
||||||
|
state.nodesInResolutionMode.has(nodeID),
|
||||||
|
);
|
||||||
|
const resolutionData = useNodeStore((state) =>
|
||||||
|
state.nodeResolutionData.get(nodeID),
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!isInResolution || !resolutionData) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(resolutionData.incompatibilities.missingOutputs);
|
||||||
|
}, [isInResolution, resolutionData]);
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ export const RightSidebar = () => {
|
|||||||
>
|
>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-200">
|
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-200">
|
||||||
Flow Debug Panel
|
Graph Debug Panel
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export const RightSidebar = () => {
|
|||||||
{l.source_id}[{l.source_name}] → {l.sink_id}[{l.sink_name}]
|
{l.source_id}[{l.source_name}] → {l.sink_id}[{l.sink_name}]
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-slate-500 dark:text-slate-400">
|
<div className="mt-1 text-slate-500 dark:text-slate-400">
|
||||||
edge_id: {l.id}
|
edge.id: {l.id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/__legacy__/ui/popover";
|
} from "@/components/__legacy__/ui/popover";
|
||||||
import { Block, BlockUIType, SpecialBlockID } from "@/lib/autogpt-server-api";
|
import {
|
||||||
|
Block,
|
||||||
|
BlockIORootSchema,
|
||||||
|
BlockUIType,
|
||||||
|
GraphInputSchema,
|
||||||
|
GraphOutputSchema,
|
||||||
|
SpecialBlockID,
|
||||||
|
} from "@/lib/autogpt-server-api";
|
||||||
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||||
import { IconToyBrick } from "@/components/__legacy__/ui/icons";
|
import { IconToyBrick } from "@/components/__legacy__/ui/icons";
|
||||||
import { getPrimaryCategoryColor } from "@/lib/utils";
|
import { getPrimaryCategoryColor } from "@/lib/utils";
|
||||||
@@ -24,8 +31,10 @@ import {
|
|||||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||||
import jaro from "jaro-winkler";
|
import jaro from "jaro-winkler";
|
||||||
|
|
||||||
type _Block = Block & {
|
type _Block = Omit<Block, "inputSchema" | "outputSchema"> & {
|
||||||
uiKey?: string;
|
uiKey?: string;
|
||||||
|
inputSchema: BlockIORootSchema | GraphInputSchema;
|
||||||
|
outputSchema: BlockIORootSchema | GraphOutputSchema;
|
||||||
hardcodedValues?: Record<string, any>;
|
hardcodedValues?: Record<string, any>;
|
||||||
_cached?: {
|
_cached?: {
|
||||||
blockName: string;
|
blockName: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/__legacy__/ui/button";
|
import { Button } from "@/components/__legacy__/ui/button";
|
||||||
import { LogOut } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
import { ClockIcon } from "@phosphor-icons/react";
|
import { ClockIcon, WarningIcon } from "@phosphor-icons/react";
|
||||||
import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons";
|
import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -13,6 +13,7 @@ interface Props {
|
|||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
resolutionModeActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BuildActionBar: React.FC<Props> = ({
|
export const BuildActionBar: React.FC<Props> = ({
|
||||||
@@ -23,9 +24,30 @@ export const BuildActionBar: React.FC<Props> = ({
|
|||||||
isRunning,
|
isRunning,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
className,
|
className,
|
||||||
|
resolutionModeActive = false,
|
||||||
}) => {
|
}) => {
|
||||||
const buttonClasses =
|
const buttonClasses =
|
||||||
"flex items-center gap-2 text-sm font-medium md:text-lg";
|
"flex items-center gap-2 text-sm font-medium md:text-lg";
|
||||||
|
|
||||||
|
// Show resolution mode message instead of action buttons
|
||||||
|
if (resolutionModeActive) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-fit select-none items-center justify-center p-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-700 dark:bg-amber-900/30">
|
||||||
|
<WarningIcon className="size-5 text-amber-600 dark:text-amber-400" />
|
||||||
|
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||||
|
Remove incompatible connections to continue
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -60,10 +60,16 @@ export function CustomEdge({
|
|||||||
targetY - 5,
|
targetY - 5,
|
||||||
);
|
);
|
||||||
const { deleteElements } = useReactFlow<Node, CustomEdge>();
|
const { deleteElements } = useReactFlow<Node, CustomEdge>();
|
||||||
const { visualizeBeads } = useContext(BuilderContext) ?? {
|
const builderContext = useContext(BuilderContext);
|
||||||
|
const { visualizeBeads } = builderContext ?? {
|
||||||
visualizeBeads: "no",
|
visualizeBeads: "no",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if this edge is broken (during resolution mode)
|
||||||
|
const isBroken =
|
||||||
|
builderContext?.resolutionMode?.active &&
|
||||||
|
builderContext?.resolutionMode?.brokenEdgeIds?.includes(id);
|
||||||
|
|
||||||
const onEdgeRemoveClick = () => {
|
const onEdgeRemoveClick = () => {
|
||||||
deleteElements({ edges: [{ id }] });
|
deleteElements({ edges: [{ id }] });
|
||||||
};
|
};
|
||||||
@@ -171,12 +177,27 @@ export function CustomEdge({
|
|||||||
|
|
||||||
const middle = getPointForT(0.5);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
path={svgPath}
|
path={svgPath}
|
||||||
markerEnd={markerEnd}
|
markerEnd={markerEnd}
|
||||||
className={`data-sentry-unmask transition-all duration-200 ${data?.isStatic ? "[stroke-dasharray:5_3]" : "[stroke-dasharray:0]"} [stroke-width:${data?.isStatic ? 2.5 : 2}px] hover:[stroke-width:${data?.isStatic ? 3.5 : 3}px] ${selected ? `[stroke:${data?.edgeColor ?? "#555555"}]` : `[stroke:${data?.edgeColor ?? "#555555"}80] hover:[stroke:${data?.edgeColor ?? "#555555"}]`}`}
|
style={{
|
||||||
|
stroke: strokeColor,
|
||||||
|
strokeWidth: data?.isStatic ? 2.5 : 2,
|
||||||
|
strokeDasharray: data?.isStatic ? "5 3" : undefined,
|
||||||
|
}}
|
||||||
|
className="data-sentry-unmask transition-all duration-200"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={svgPath}
|
d={svgPath}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
BlockIOSubSchema,
|
BlockIOSubSchema,
|
||||||
BlockUIType,
|
BlockUIType,
|
||||||
Category,
|
Category,
|
||||||
|
GraphInputSchema,
|
||||||
|
GraphOutputSchema,
|
||||||
NodeExecutionResult,
|
NodeExecutionResult,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
import {
|
import {
|
||||||
@@ -62,14 +64,21 @@ import { NodeGenericInputField, NodeTextBoxInput } from "../NodeInputs";
|
|||||||
import NodeOutputs from "../NodeOutputs";
|
import NodeOutputs from "../NodeOutputs";
|
||||||
import OutputModalComponent from "../OutputModalComponent";
|
import OutputModalComponent from "../OutputModalComponent";
|
||||||
import "./customnode.css";
|
import "./customnode.css";
|
||||||
|
import { SubAgentUpdateBar } from "./SubAgentUpdateBar";
|
||||||
|
import { IncompatibilityDialog } from "./IncompatibilityDialog";
|
||||||
|
import {
|
||||||
|
useSubAgentUpdate,
|
||||||
|
createUpdatedAgentNodeInputs,
|
||||||
|
getBrokenEdgeIDs,
|
||||||
|
} from "../../../hooks/useSubAgentUpdate";
|
||||||
|
|
||||||
export type ConnectionData = Array<{
|
export type ConnectedEdge = {
|
||||||
edge_id: string;
|
id: string;
|
||||||
source: string;
|
source: string;
|
||||||
sourceHandle: string;
|
sourceHandle: string;
|
||||||
target: string;
|
target: string;
|
||||||
targetHandle: string;
|
targetHandle: string;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
export type CustomNodeData = {
|
export type CustomNodeData = {
|
||||||
blockType: string;
|
blockType: string;
|
||||||
@@ -80,7 +89,7 @@ export type CustomNodeData = {
|
|||||||
inputSchema: BlockIORootSchema;
|
inputSchema: BlockIORootSchema;
|
||||||
outputSchema: BlockIORootSchema;
|
outputSchema: BlockIORootSchema;
|
||||||
hardcodedValues: { [key: string]: any };
|
hardcodedValues: { [key: string]: any };
|
||||||
connections: ConnectionData;
|
connections: ConnectedEdge[];
|
||||||
isOutputOpen: boolean;
|
isOutputOpen: boolean;
|
||||||
status?: NodeExecutionResult["status"];
|
status?: NodeExecutionResult["status"];
|
||||||
/** executionResults contains outputs across multiple executions
|
/** executionResults contains outputs across multiple executions
|
||||||
@@ -127,20 +136,199 @@ export const CustomNode = React.memo(
|
|||||||
|
|
||||||
let subGraphID = "";
|
let subGraphID = "";
|
||||||
|
|
||||||
if (data.uiType === BlockUIType.AGENT) {
|
|
||||||
// Display the graph's schema instead AgentExecutorBlock's schema.
|
|
||||||
data.inputSchema = data.hardcodedValues?.input_schema || {};
|
|
||||||
data.outputSchema = data.hardcodedValues?.output_schema || {};
|
|
||||||
subGraphID = data.hardcodedValues?.graph_id || subGraphID;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!builderContext) {
|
if (!builderContext) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"BuilderContext consumer must be inside FlowEditor component",
|
"BuilderContext consumer must be inside FlowEditor component",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { libraryAgent, setIsAnyModalOpen, getNextNodeId } = builderContext;
|
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(() => {
|
useEffect(() => {
|
||||||
if (data.executionResults || data.status) {
|
if (data.executionResults || data.status) {
|
||||||
@@ -156,13 +344,6 @@ export const CustomNode = React.memo(
|
|||||||
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
||||||
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
|
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
|
||||||
|
|
||||||
const setHardcodedValues = useCallback(
|
|
||||||
(values: any) => {
|
|
||||||
updateNodeData(id, { hardcodedValues: values });
|
|
||||||
},
|
|
||||||
[id, updateNodeData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTitleEdit = useCallback(() => {
|
const handleTitleEdit = useCallback(() => {
|
||||||
setIsEditingTitle(true);
|
setIsEditingTitle(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -255,6 +436,7 @@ export const CustomNode = React.memo(
|
|||||||
isConnected={isOutputHandleConnected(propKey)}
|
isConnected={isOutputHandleConnected(propKey)}
|
||||||
schema={fieldSchema}
|
schema={fieldSchema}
|
||||||
side="right"
|
side="right"
|
||||||
|
isBroken={isOutputHandleBroken(propKey)}
|
||||||
/>
|
/>
|
||||||
{"properties" in fieldSchema &&
|
{"properties" in fieldSchema &&
|
||||||
renderHandles(
|
renderHandles(
|
||||||
@@ -385,6 +567,7 @@ export const CustomNode = React.memo(
|
|||||||
isRequired={isRequired}
|
isRequired={isRequired}
|
||||||
schema={propSchema}
|
schema={propSchema}
|
||||||
side="left"
|
side="left"
|
||||||
|
isBroken={isInputHandleBroken(propKey)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
propKey !== "credentials" &&
|
propKey !== "credentials" &&
|
||||||
@@ -873,6 +1056,22 @@ export const CustomNode = React.memo(
|
|||||||
<ContextMenuContent />
|
<ContextMenuContent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sub-agent Update Bar - shown below header */}
|
||||||
|
{isAgentBlock && (subAgentUpdate.hasUpdate || isInResolutionMode) && (
|
||||||
|
<SubAgentUpdateBar
|
||||||
|
currentVersion={subAgentUpdate.currentVersion}
|
||||||
|
latestVersion={subAgentUpdate.latestVersion}
|
||||||
|
isCompatible={subAgentUpdate.isCompatible}
|
||||||
|
incompatibilities={
|
||||||
|
isInResolutionMode
|
||||||
|
? resolutionMode.incompatibilities
|
||||||
|
: subAgentUpdate.incompatibilities
|
||||||
|
}
|
||||||
|
onUpdate={handleUpdateClick}
|
||||||
|
isInResolutionMode={isInResolutionMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="mx-5 my-6 rounded-b-xl">
|
<div className="mx-5 my-6 rounded-b-xl">
|
||||||
{/* Input Handles */}
|
{/* Input Handles */}
|
||||||
@@ -1044,9 +1243,24 @@ export const CustomNode = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ContextMenu.Root>
|
<ContextMenu.Root>
|
||||||
<ContextMenu.Trigger>{nodeContent()}</ContextMenu.Trigger>
|
<ContextMenu.Trigger>{nodeContent()}</ContextMenu.Trigger>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
|
|
||||||
|
{/* Incompatibility Dialog for sub-agent updates */}
|
||||||
|
{isAgentBlock && subAgentUpdate.incompatibilities && (
|
||||||
|
<IncompatibilityDialog
|
||||||
|
isOpen={showIncompatibilityDialog}
|
||||||
|
onClose={() => setShowIncompatibilityDialog(false)}
|
||||||
|
onConfirm={handleConfirmIncompatibleUpdate}
|
||||||
|
currentVersion={subAgentUpdate.currentVersion}
|
||||||
|
latestVersion={subAgentUpdate.latestVersion}
|
||||||
|
agentName={data.blockType || "Agent"}
|
||||||
|
incompatibilities={subAgentUpdate.incompatibilities}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prevProps, nextProps) => {
|
(prevProps, nextProps) => {
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
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<IncompatibilityDialogProps> = ({
|
||||||
|
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 (
|
||||||
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
||||||
|
Incompatible Update
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Updating <strong>{beautifyString(agentName)}</strong> from v
|
||||||
|
{currentVersion} to v{latestVersion} will break some connections.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
{/* Input changes - two column layout */}
|
||||||
|
{hasInputChanges && (
|
||||||
|
<TwoColumnSection
|
||||||
|
title="Input Changes"
|
||||||
|
leftIcon={<XCircle className="h-4 w-4 text-red-500" />}
|
||||||
|
leftTitle="Removed"
|
||||||
|
leftItems={incompatibilities.missingInputs}
|
||||||
|
rightIcon={<PlusCircle className="h-4 w-4 text-green-500" />}
|
||||||
|
rightTitle="Added"
|
||||||
|
rightItems={incompatibilities.newInputs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Output changes - two column layout */}
|
||||||
|
{hasOutputChanges && (
|
||||||
|
<TwoColumnSection
|
||||||
|
title="Output Changes"
|
||||||
|
leftIcon={<XCircle className="h-4 w-4 text-red-500" />}
|
||||||
|
leftTitle="Removed"
|
||||||
|
leftItems={incompatibilities.missingOutputs}
|
||||||
|
rightIcon={<PlusCircle className="h-4 w-4 text-green-500" />}
|
||||||
|
rightTitle="Added"
|
||||||
|
rightItems={incompatibilities.newOutputs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasTypeMismatches && (
|
||||||
|
<SingleColumnSection
|
||||||
|
icon={<XCircle className="h-4 w-4 text-red-500" />}
|
||||||
|
title="Type Changed"
|
||||||
|
description="These connected inputs have a different type:"
|
||||||
|
items={incompatibilities.inputTypeMismatches.map(
|
||||||
|
(m) => `${m.name} (${m.oldType} → ${m.newType})`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasNewRequired && (
|
||||||
|
<SingleColumnSection
|
||||||
|
icon={<PlusCircle className="h-4 w-4 text-amber-500" />}
|
||||||
|
title="New Required Inputs"
|
||||||
|
description="These inputs are now required:"
|
||||||
|
items={incompatibilities.newRequiredInputs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
If you proceed, you'll need to remove the broken connections
|
||||||
|
before you can save or run your agent.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={onConfirm}
|
||||||
|
className="bg-amber-600 hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
Update Anyway
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TwoColumnSectionProps {
|
||||||
|
title: string;
|
||||||
|
leftIcon: React.ReactNode;
|
||||||
|
leftTitle: string;
|
||||||
|
leftItems: string[];
|
||||||
|
rightIcon: React.ReactNode;
|
||||||
|
rightTitle: string;
|
||||||
|
rightItems: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TwoColumnSection: React.FC<TwoColumnSectionProps> = ({
|
||||||
|
title,
|
||||||
|
leftIcon,
|
||||||
|
leftTitle,
|
||||||
|
leftItems,
|
||||||
|
rightIcon,
|
||||||
|
rightTitle,
|
||||||
|
rightItems,
|
||||||
|
}) => (
|
||||||
|
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
<span className="font-medium">{title}</span>
|
||||||
|
<div className="mt-2 grid grid-cols-2 items-start gap-4">
|
||||||
|
{/* Left column - Breaking changes */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{leftIcon}
|
||||||
|
<span>{leftTitle}</span>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-1.5 space-y-1">
|
||||||
|
{leftItems.length > 0 ? (
|
||||||
|
leftItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<code className="rounded bg-red-50 px-1 py-0.5 font-mono text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
|
||||||
|
{item}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
||||||
|
None
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column - Possible solutions */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{rightIcon}
|
||||||
|
<span>{rightTitle}</span>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-1.5 space-y-1">
|
||||||
|
{rightItems.length > 0 ? (
|
||||||
|
rightItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<code className="rounded bg-green-50 px-1 py-0.5 font-mono text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||||
|
{item}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
||||||
|
None
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SingleColumnSectionProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleColumnSection: React.FC<SingleColumnSectionProps> = ({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
items,
|
||||||
|
}) => (
|
||||||
|
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icon}
|
||||||
|
<span className="font-medium">{title}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 space-y-1">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="ml-4 list-disc text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs dark:bg-gray-800">
|
||||||
|
{item}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default IncompatibilityDialog;
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
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<SubAgentUpdateBarProps> = ({
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
isCompatible,
|
||||||
|
incompatibilities,
|
||||||
|
onUpdate,
|
||||||
|
isInResolutionMode = false,
|
||||||
|
}) => {
|
||||||
|
if (isInResolutionMode) {
|
||||||
|
return <ResolutionModeBar incompatibilities={incompatibilities} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-t-lg bg-blue-50 px-3 py-2 dark:bg-blue-900/30">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArrowUp className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
<span className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Update available (v{currentVersion} → v{latestVersion})
|
||||||
|
</span>
|
||||||
|
{!isCompatible && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<p className="font-medium">Incompatible changes detected</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Click Update to see details
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={isCompatible ? "default" : "outline"}
|
||||||
|
onClick={onUpdate}
|
||||||
|
className={cn(
|
||||||
|
"h-7 text-xs",
|
||||||
|
!isCompatible && "border-amber-500 text-amber-600 hover:bg-amber-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ResolutionModeBarProps {
|
||||||
|
incompatibilities: IncompatibilityInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResolutionModeBar: React.FC<ResolutionModeBarProps> = ({
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-t-lg bg-amber-50 px-3 py-2 dark:bg-amber-900/30">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
<span className="text-sm text-amber-700 dark:text-amber-300">
|
||||||
|
Remove incompatible connections
|
||||||
|
</span>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="h-4 w-4 cursor-help text-amber-500" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-sm whitespace-pre-line">
|
||||||
|
<p className="font-medium">Incompatible changes:</p>
|
||||||
|
<p className="mt-1 text-xs">{formatIncompatibilities()}</p>
|
||||||
|
<p className="mt-2 text-xs text-gray-400">
|
||||||
|
Delete the red connections to continue
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubAgentUpdateBar;
|
||||||
@@ -26,15 +26,17 @@ import {
|
|||||||
applyNodeChanges,
|
applyNodeChanges,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { CustomNode } from "../CustomNode/CustomNode";
|
import { ConnectedEdge, CustomNode } from "../CustomNode/CustomNode";
|
||||||
import "./flow.css";
|
import "./flow.css";
|
||||||
import {
|
import {
|
||||||
BlockUIType,
|
BlockUIType,
|
||||||
formatEdgeID,
|
formatEdgeID,
|
||||||
GraphExecutionID,
|
GraphExecutionID,
|
||||||
GraphID,
|
GraphID,
|
||||||
|
GraphMeta,
|
||||||
LibraryAgent,
|
LibraryAgent,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
|
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
|
||||||
import { Key, storage } from "@/services/storage/local-storage";
|
import { Key, storage } from "@/services/storage/local-storage";
|
||||||
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
|
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
|
||||||
import { history } from "../history";
|
import { history } from "../history";
|
||||||
@@ -72,12 +74,30 @@ import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle";
|
|||||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
// 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;
|
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||||
|
|
||||||
|
export type ResolutionModeState = {
|
||||||
|
active: boolean;
|
||||||
|
nodeId: string | null;
|
||||||
|
incompatibilities: IncompatibilityInfo | null;
|
||||||
|
brokenEdgeIds: string[];
|
||||||
|
pendingUpdate: Record<string, unknown> | null; // The hardcoded values to apply after resolution
|
||||||
|
};
|
||||||
|
|
||||||
type BuilderContextType = {
|
type BuilderContextType = {
|
||||||
libraryAgent: LibraryAgent | null;
|
libraryAgent: LibraryAgent | null;
|
||||||
visualizeBeads: "no" | "static" | "animate";
|
visualizeBeads: "no" | "static" | "animate";
|
||||||
setIsAnyModalOpen: (isOpen: boolean) => void;
|
setIsAnyModalOpen: (isOpen: boolean) => void;
|
||||||
getNextNodeId: () => string;
|
getNextNodeId: () => string;
|
||||||
getNodeTitle: (nodeID: string) => string | null;
|
getNodeTitle: (nodeID: string) => string | null;
|
||||||
|
availableFlows: GraphMeta[];
|
||||||
|
resolutionMode: ResolutionModeState;
|
||||||
|
enterResolutionMode: (
|
||||||
|
nodeId: string,
|
||||||
|
incompatibilities: IncompatibilityInfo,
|
||||||
|
brokenEdgeIds: string[],
|
||||||
|
pendingUpdate: Record<string, unknown>,
|
||||||
|
) => void;
|
||||||
|
exitResolutionMode: () => void;
|
||||||
|
applyPendingUpdate: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeDimension = {
|
export type NodeDimension = {
|
||||||
@@ -172,6 +192,92 @@ const FlowEditor: React.FC<{
|
|||||||
// It stores the dimension of all nodes with position as well
|
// It stores the dimension of all nodes with position as well
|
||||||
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});
|
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});
|
||||||
|
|
||||||
|
// Resolution mode state for sub-agent incompatible updates
|
||||||
|
const [resolutionMode, setResolutionMode] = useState<ResolutionModeState>({
|
||||||
|
active: false,
|
||||||
|
nodeId: null,
|
||||||
|
incompatibilities: null,
|
||||||
|
brokenEdgeIds: [],
|
||||||
|
pendingUpdate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const enterResolutionMode = useCallback(
|
||||||
|
(
|
||||||
|
nodeId: string,
|
||||||
|
incompatibilities: IncompatibilityInfo,
|
||||||
|
brokenEdgeIds: string[],
|
||||||
|
pendingUpdate: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
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
|
// Set page title with or without graph name
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = savedAgent
|
document.title = savedAgent
|
||||||
@@ -431,17 +537,19 @@ const FlowEditor: React.FC<{
|
|||||||
...node.data.connections.filter(
|
...node.data.connections.filter(
|
||||||
(conn) =>
|
(conn) =>
|
||||||
!removedEdges.some(
|
!removedEdges.some(
|
||||||
(removedEdge) => removedEdge.id === conn.edge_id,
|
(removedEdge) => removedEdge.id === conn.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Add node connections for added edges
|
// Add node connections for added edges
|
||||||
...addedEdges.map((addedEdge) => ({
|
...addedEdges.map(
|
||||||
edge_id: addedEdge.item.id,
|
(addedEdge): ConnectedEdge => ({
|
||||||
|
id: addedEdge.item.id,
|
||||||
source: addedEdge.item.source,
|
source: addedEdge.item.source,
|
||||||
target: addedEdge.item.target,
|
target: addedEdge.item.target,
|
||||||
sourceHandle: addedEdge.item.sourceHandle!,
|
sourceHandle: addedEdge.item.sourceHandle!,
|
||||||
targetHandle: addedEdge.item.targetHandle!,
|
targetHandle: addedEdge.item.targetHandle!,
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -467,13 +575,15 @@ const FlowEditor: React.FC<{
|
|||||||
data: {
|
data: {
|
||||||
...node.data,
|
...node.data,
|
||||||
connections: [
|
connections: [
|
||||||
...replaceEdges.map((replaceEdge) => ({
|
...replaceEdges.map(
|
||||||
edge_id: replaceEdge.item.id,
|
(replaceEdge): ConnectedEdge => ({
|
||||||
|
id: replaceEdge.item.id,
|
||||||
source: replaceEdge.item.source,
|
source: replaceEdge.item.source,
|
||||||
target: replaceEdge.item.target,
|
target: replaceEdge.item.target,
|
||||||
sourceHandle: replaceEdge.item.sourceHandle!,
|
sourceHandle: replaceEdge.item.sourceHandle!,
|
||||||
targetHandle: replaceEdge.item.targetHandle!,
|
targetHandle: replaceEdge.item.targetHandle!,
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
@@ -890,8 +1000,23 @@ const FlowEditor: React.FC<{
|
|||||||
setIsAnyModalOpen,
|
setIsAnyModalOpen,
|
||||||
getNextNodeId,
|
getNextNodeId,
|
||||||
getNodeTitle,
|
getNodeTitle,
|
||||||
|
availableFlows,
|
||||||
|
resolutionMode,
|
||||||
|
enterResolutionMode,
|
||||||
|
exitResolutionMode,
|
||||||
|
applyPendingUpdate,
|
||||||
}),
|
}),
|
||||||
[libraryAgent, visualizeBeads, getNextNodeId, getNodeTitle],
|
[
|
||||||
|
libraryAgent,
|
||||||
|
visualizeBeads,
|
||||||
|
getNextNodeId,
|
||||||
|
getNodeTitle,
|
||||||
|
availableFlows,
|
||||||
|
resolutionMode,
|
||||||
|
enterResolutionMode,
|
||||||
|
applyPendingUpdate,
|
||||||
|
exitResolutionMode,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -991,6 +1116,7 @@ const FlowEditor: React.FC<{
|
|||||||
onClickScheduleButton={handleScheduleButton}
|
onClickScheduleButton={handleScheduleButton}
|
||||||
isDisabled={!savedAgent}
|
isDisabled={!savedAgent}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
|
resolutionModeActive={resolutionMode.active}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">
|
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
|
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
|
||||||
import { cn } from "@/lib/utils";
|
import {
|
||||||
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
|
cn,
|
||||||
|
beautifyString,
|
||||||
|
getTypeBgColor,
|
||||||
|
getTypeTextColor,
|
||||||
|
getEffectiveType,
|
||||||
|
} from "@/lib/utils";
|
||||||
import { FC, memo, useCallback } from "react";
|
import { FC, memo, useCallback } from "react";
|
||||||
import { Handle, Position } from "@xyflow/react";
|
import { Handle, Position } from "@xyflow/react";
|
||||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||||
@@ -13,6 +18,7 @@ type HandleProps = {
|
|||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
isBroken?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move the constant out of the component to avoid re-creation on every render.
|
// Move the constant out of the component to avoid re-creation on every render.
|
||||||
@@ -27,18 +33,23 @@ const TYPE_NAME: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
|
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
|
||||||
const Dot: FC<{ isConnected: boolean; type?: string }> = memo(
|
const Dot: FC<{ isConnected: boolean; type?: string; isBroken?: boolean }> =
|
||||||
({ isConnected, type }) => {
|
memo(({ isConnected, type, isBroken }) => {
|
||||||
const color = isConnected
|
const color = isBroken
|
||||||
|
? "border-red-500 bg-red-100 dark:bg-red-900/30"
|
||||||
|
: isConnected
|
||||||
? getTypeBgColor(type || "any")
|
? getTypeBgColor(type || "any")
|
||||||
: "border-gray-300 dark:border-gray-600";
|
: "border-gray-300 dark:border-gray-600";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
|
className={cn(
|
||||||
|
"m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700",
|
||||||
|
color,
|
||||||
|
isBroken && "opacity-50",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
Dot.displayName = "Dot";
|
Dot.displayName = "Dot";
|
||||||
|
|
||||||
const NodeHandle: FC<HandleProps> = ({
|
const NodeHandle: FC<HandleProps> = ({
|
||||||
@@ -49,24 +60,34 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
side,
|
side,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
|
isBroken = false,
|
||||||
}) => {
|
}) => {
|
||||||
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${
|
// Extract effective type from schema (handles anyOf/oneOf/allOf wrappers)
|
||||||
|
const effectiveType = getEffectiveType(schema);
|
||||||
|
|
||||||
|
const typeClass = `text-sm ${getTypeTextColor(effectiveType || "any")} ${
|
||||||
side === "left" ? "text-left" : "text-right"
|
side === "left" ? "text-left" : "text-right"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const label = (
|
const label = (
|
||||||
<div className="flex flex-grow flex-row">
|
<div className={cn("flex flex-grow flex-row", isBroken && "opacity-50")}>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-sentry-unmask text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100",
|
"data-sentry-unmask text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100",
|
||||||
className,
|
className,
|
||||||
|
isBroken && "text-red-500 line-through",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title || schema.title || beautifyString(keyName.toLowerCase())}
|
{title || schema.title || beautifyString(keyName.toLowerCase())}
|
||||||
{isRequired ? "*" : ""}
|
{isRequired ? "*" : ""}
|
||||||
</span>
|
</span>
|
||||||
<span className={`${typeClass} data-sentry-unmask flex items-end`}>
|
<span
|
||||||
({TYPE_NAME[schema.type as keyof typeof TYPE_NAME] || "any"})
|
className={cn(
|
||||||
|
`${typeClass} data-sentry-unmask flex items-end`,
|
||||||
|
isBroken && "text-red-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
({TYPE_NAME[effectiveType as keyof typeof TYPE_NAME] || "any"})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -84,7 +105,7 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={keyName}
|
key={keyName}
|
||||||
className="handle-container"
|
className={cn("handle-container", isBroken && "pointer-events-none")}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -92,10 +113,15 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
data-testid={`input-handle-${keyName}`}
|
data-testid={`input-handle-${keyName}`}
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id={keyName}
|
id={keyName}
|
||||||
className="group -ml-[38px]"
|
className={cn("group -ml-[38px]", isBroken && "cursor-not-allowed")}
|
||||||
|
isConnectable={!isBroken}
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
<Dot isConnected={isConnected} type={schema.type} />
|
<Dot
|
||||||
|
isConnected={isConnected}
|
||||||
|
type={effectiveType}
|
||||||
|
isBroken={isBroken}
|
||||||
|
/>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
@@ -106,7 +132,10 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={keyName}
|
key={keyName}
|
||||||
className="handle-container justify-end"
|
className={cn(
|
||||||
|
"handle-container justify-end",
|
||||||
|
isBroken && "pointer-events-none",
|
||||||
|
)}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -114,11 +143,16 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
data-testid={`output-handle-${keyName}`}
|
data-testid={`output-handle-${keyName}`}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id={keyName}
|
id={keyName}
|
||||||
className="group -mr-[38px]"
|
className={cn("group -mr-[38px]", isBroken && "cursor-not-allowed")}
|
||||||
|
isConnectable={!isBroken}
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
{label}
|
{label}
|
||||||
<Dot isConnected={isConnected} type={schema.type} />
|
<Dot
|
||||||
|
isConnected={isConnected}
|
||||||
|
type={effectiveType}
|
||||||
|
isBroken={isBroken}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ConnectionData,
|
ConnectedEdge,
|
||||||
CustomNodeData,
|
CustomNodeData,
|
||||||
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
|
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
|
||||||
import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput";
|
import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput";
|
||||||
@@ -65,7 +65,7 @@ type NodeObjectInputTreeProps = {
|
|||||||
selfKey?: string;
|
selfKey?: string;
|
||||||
schema: BlockIORootSchema | BlockIOObjectSubSchema;
|
schema: BlockIORootSchema | BlockIOObjectSubSchema;
|
||||||
object?: { [key: string]: any };
|
object?: { [key: string]: any };
|
||||||
connections: ConnectionData;
|
connections: ConnectedEdge[];
|
||||||
handleInputClick: (key: string) => void;
|
handleInputClick: (key: string) => void;
|
||||||
handleInputChange: (key: string, value: any) => void;
|
handleInputChange: (key: string, value: any) => void;
|
||||||
errors: { [key: string]: string | undefined };
|
errors: { [key: string]: string | undefined };
|
||||||
@@ -585,7 +585,7 @@ const NodeOneOfDiscriminatorField: FC<{
|
|||||||
currentValue?: any;
|
currentValue?: any;
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
errors: { [key: string]: string | undefined };
|
errors: { [key: string]: string | undefined };
|
||||||
connections: ConnectionData;
|
connections: ConnectedEdge[];
|
||||||
handleInputChange: (key: string, value: any) => void;
|
handleInputChange: (key: string, value: any) => void;
|
||||||
handleInputClick: (key: string) => void;
|
handleInputClick: (key: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from "react";
|
import { FC, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import NodeHandle from "@/app/(platform)/build/components/legacy-builder/NodeHandle";
|
import NodeHandle from "@/app/(platform)/build/components/legacy-builder/NodeHandle";
|
||||||
import {
|
import type {
|
||||||
BlockIOTableSubSchema,
|
BlockIOTableSubSchema,
|
||||||
TableCellValue,
|
TableCellValue,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
|
import type { ConnectedEdge } from "./CustomNode/CustomNode";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
||||||
import { Button } from "../../../../../components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { Input } from "../../../../../components/atoms/Input/Input";
|
import { Input } from "@/components/atoms/Input/Input";
|
||||||
|
|
||||||
interface NodeTableInputProps {
|
interface NodeTableInputProps {
|
||||||
/** Unique identifier for the node in the builder graph */
|
/** Unique identifier for the node in the builder graph */
|
||||||
@@ -25,13 +26,7 @@ interface NodeTableInputProps {
|
|||||||
/** Validation errors mapped by field key */
|
/** Validation errors mapped by field key */
|
||||||
errors: { [key: string]: string | undefined };
|
errors: { [key: string]: string | undefined };
|
||||||
/** Graph connections between nodes in the builder */
|
/** Graph connections between nodes in the builder */
|
||||||
connections: {
|
connections: ConnectedEdge[];
|
||||||
edge_id: string;
|
|
||||||
source: string;
|
|
||||||
sourceHandle: string;
|
|
||||||
target: string;
|
|
||||||
targetHandle: string;
|
|
||||||
}[];
|
|
||||||
/** Callback when table data changes */
|
/** Callback when table data changes */
|
||||||
handleInputChange: (key: string, value: TableRow[]) => void;
|
handleInputChange: (key: string, value: TableRow[]) => void;
|
||||||
/** Callback when input field is clicked (for builder selection) */
|
/** Callback when input field is clicked (for builder selection) */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Node, Edge, useReactFlow } from "@xyflow/react";
|
import { Node, Edge, useReactFlow } from "@xyflow/react";
|
||||||
import { Key, storage } from "@/services/storage/local-storage";
|
import { Key, storage } from "@/services/storage/local-storage";
|
||||||
|
import { ConnectedEdge } from "./CustomNode/CustomNode";
|
||||||
|
|
||||||
interface CopyableData {
|
interface CopyableData {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
@@ -111,13 +112,15 @@ export function useCopyPaste(getNextNodeId: () => string) {
|
|||||||
(edge: Edge) =>
|
(edge: Edge) =>
|
||||||
edge.source === node.id || edge.target === node.id,
|
edge.source === node.id || edge.target === node.id,
|
||||||
)
|
)
|
||||||
.map((edge: Edge) => ({
|
.map(
|
||||||
edge_id: edge.id,
|
(edge: Edge): ConnectedEdge => ({
|
||||||
|
id: edge.id,
|
||||||
source: edge.source,
|
source: edge.source,
|
||||||
target: edge.target,
|
target: edge.target,
|
||||||
sourceHandle: edge.sourceHandle,
|
sourceHandle: edge.sourceHandle!,
|
||||||
targetHandle: edge.targetHandle,
|
targetHandle: edge.targetHandle!,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { GraphInputSchema } from "@/lib/autogpt-server-api";
|
||||||
|
import { GraphMetaLike, IncompatibilityInfo } from "./types";
|
||||||
|
|
||||||
|
// Helper type for schema properties - the generated types are too loose
|
||||||
|
type SchemaProperties = Record<string, GraphInputSchema["properties"][string]>;
|
||||||
|
type SchemaRequired = string[];
|
||||||
|
|
||||||
|
// Helper to safely extract schema properties
|
||||||
|
export function getSchemaProperties(schema: unknown): SchemaProperties {
|
||||||
|
if (
|
||||||
|
schema &&
|
||||||
|
typeof schema === "object" &&
|
||||||
|
"properties" in schema &&
|
||||||
|
typeof schema.properties === "object" &&
|
||||||
|
schema.properties !== null
|
||||||
|
) {
|
||||||
|
return schema.properties as SchemaProperties;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSchemaRequired(schema: unknown): SchemaRequired {
|
||||||
|
if (
|
||||||
|
schema &&
|
||||||
|
typeof schema === "object" &&
|
||||||
|
"required" in schema &&
|
||||||
|
Array.isArray(schema.required)
|
||||||
|
) {
|
||||||
|
return schema.required as SchemaRequired;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the updated agent node inputs for a sub-agent node
|
||||||
|
*/
|
||||||
|
export function createUpdatedAgentNodeInputs(
|
||||||
|
currentInputs: Record<string, unknown>,
|
||||||
|
latestSubGraphVersion: GraphMetaLike,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...currentInputs,
|
||||||
|
graph_version: latestSubGraphVersion.version,
|
||||||
|
input_schema: latestSubGraphVersion.input_schema,
|
||||||
|
output_schema: latestSubGraphVersion.output_schema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic edge type that works with both builders:
|
||||||
|
* - New builder uses CustomEdge with (formally) optional handles
|
||||||
|
* - Legacy builder uses ConnectedEdge type with required handles */
|
||||||
|
export type EdgeLike = {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
sourceHandle?: string | null;
|
||||||
|
targetHandle?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines which edges are broken after an incompatible update.
|
||||||
|
* Works with both legacy ConnectedEdge and new CustomEdge.
|
||||||
|
*/
|
||||||
|
export function getBrokenEdgeIDs(
|
||||||
|
connections: EdgeLike[],
|
||||||
|
incompatibilities: IncompatibilityInfo,
|
||||||
|
nodeID: string,
|
||||||
|
): string[] {
|
||||||
|
const brokenEdgeIDs: string[] = [];
|
||||||
|
const typeMismatchInputNames = new Set(
|
||||||
|
incompatibilities.inputTypeMismatches.map((m) => m.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
connections.forEach((conn) => {
|
||||||
|
// Check if this connection uses a missing input (node is target)
|
||||||
|
if (
|
||||||
|
conn.target === nodeID &&
|
||||||
|
conn.targetHandle &&
|
||||||
|
incompatibilities.missingInputs.includes(conn.targetHandle)
|
||||||
|
) {
|
||||||
|
brokenEdgeIDs.push(conn.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this connection uses an input with a type mismatch (node is target)
|
||||||
|
if (
|
||||||
|
conn.target === nodeID &&
|
||||||
|
conn.targetHandle &&
|
||||||
|
typeMismatchInputNames.has(conn.targetHandle)
|
||||||
|
) {
|
||||||
|
brokenEdgeIDs.push(conn.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this connection uses a missing output (node is source)
|
||||||
|
if (
|
||||||
|
conn.source === nodeID &&
|
||||||
|
conn.sourceHandle &&
|
||||||
|
incompatibilities.missingOutputs.includes(conn.sourceHandle)
|
||||||
|
) {
|
||||||
|
brokenEdgeIDs.push(conn.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return brokenEdgeIDs;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useSubAgentUpdate } from "./useSubAgentUpdate";
|
||||||
|
export { createUpdatedAgentNodeInputs, getBrokenEdgeIDs } from "./helpers";
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { GraphMeta as LegacyGraphMeta } from "@/lib/autogpt-server-api";
|
||||||
|
import type { GraphMeta as GeneratedGraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||||
|
|
||||||
|
export type SubAgentUpdateInfo<T extends GraphMetaLike = GraphMetaLike> = {
|
||||||
|
hasUpdate: boolean;
|
||||||
|
currentVersion: number;
|
||||||
|
latestVersion: number;
|
||||||
|
latestGraph: T | null;
|
||||||
|
isCompatible: boolean;
|
||||||
|
incompatibilities: IncompatibilityInfo | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Union type for GraphMeta that works with both legacy and new builder
|
||||||
|
export type GraphMetaLike = LegacyGraphMeta | GeneratedGraphMeta;
|
||||||
|
|
||||||
|
export type IncompatibilityInfo = {
|
||||||
|
missingInputs: string[]; // Connected inputs that no longer exist
|
||||||
|
missingOutputs: string[]; // Connected outputs that no longer exist
|
||||||
|
newInputs: string[]; // Inputs that exist in new version but not in current
|
||||||
|
newOutputs: string[]; // Outputs that exist in new version but not in current
|
||||||
|
newRequiredInputs: string[]; // New required inputs not in current version or not required
|
||||||
|
inputTypeMismatches: Array<{
|
||||||
|
name: string;
|
||||||
|
oldType: string;
|
||||||
|
newType: string;
|
||||||
|
}>; // Connected inputs where the type has changed
|
||||||
|
};
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
||||||
|
import { getEffectiveType } from "@/lib/utils";
|
||||||
|
import { EdgeLike, getSchemaProperties, getSchemaRequired } from "./helpers";
|
||||||
|
import {
|
||||||
|
GraphMetaLike,
|
||||||
|
IncompatibilityInfo,
|
||||||
|
SubAgentUpdateInfo,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a newer version of a sub-agent is available and determines compatibility
|
||||||
|
*/
|
||||||
|
export function useSubAgentUpdate<T extends GraphMetaLike>(
|
||||||
|
nodeID: string,
|
||||||
|
graphID: string | undefined,
|
||||||
|
graphVersion: number | undefined,
|
||||||
|
currentInputSchema: GraphInputSchema | undefined,
|
||||||
|
currentOutputSchema: GraphOutputSchema | undefined,
|
||||||
|
connections: EdgeLike[],
|
||||||
|
availableGraphs: T[],
|
||||||
|
): SubAgentUpdateInfo<T> {
|
||||||
|
// Find the latest version of the same graph
|
||||||
|
const latestGraph = useMemo(() => {
|
||||||
|
if (!graphID) return null;
|
||||||
|
return availableGraphs.find((graph) => graph.id === graphID) || null;
|
||||||
|
}, [graphID, availableGraphs]);
|
||||||
|
|
||||||
|
// Check if there's an update available
|
||||||
|
const hasUpdate = useMemo(() => {
|
||||||
|
if (!latestGraph || graphVersion === undefined) return false;
|
||||||
|
return latestGraph.version! > graphVersion;
|
||||||
|
}, [latestGraph, graphVersion]);
|
||||||
|
|
||||||
|
// Get connected input and output handles for this specific node
|
||||||
|
const connectedHandles = useMemo(() => {
|
||||||
|
const inputHandles = new Set<string>();
|
||||||
|
const outputHandles = new Set<string>();
|
||||||
|
|
||||||
|
connections.forEach((conn) => {
|
||||||
|
// If this node is the target, the targetHandle is an input on this node
|
||||||
|
if (conn.target === nodeID && conn.targetHandle) {
|
||||||
|
inputHandles.add(conn.targetHandle);
|
||||||
|
}
|
||||||
|
// If this node is the source, the sourceHandle is an output on this node
|
||||||
|
if (conn.source === nodeID && conn.sourceHandle) {
|
||||||
|
outputHandles.add(conn.sourceHandle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { inputHandles, outputHandles };
|
||||||
|
}, [connections, nodeID]);
|
||||||
|
|
||||||
|
// Check schema compatibility
|
||||||
|
const compatibilityResult = useMemo((): {
|
||||||
|
isCompatible: boolean;
|
||||||
|
incompatibilities: IncompatibilityInfo | null;
|
||||||
|
} => {
|
||||||
|
if (!hasUpdate || !latestGraph) {
|
||||||
|
return { isCompatible: true, incompatibilities: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newInputProps = getSchemaProperties(latestGraph.input_schema);
|
||||||
|
const newOutputProps = getSchemaProperties(latestGraph.output_schema);
|
||||||
|
const newRequiredInputs = getSchemaRequired(latestGraph.input_schema);
|
||||||
|
|
||||||
|
const currentInputProps = getSchemaProperties(currentInputSchema);
|
||||||
|
const currentOutputProps = getSchemaProperties(currentOutputSchema);
|
||||||
|
const currentRequiredInputs = getSchemaRequired(currentInputSchema);
|
||||||
|
|
||||||
|
const incompatibilities: IncompatibilityInfo = {
|
||||||
|
missingInputs: [],
|
||||||
|
missingOutputs: [],
|
||||||
|
newInputs: [],
|
||||||
|
newOutputs: [],
|
||||||
|
newRequiredInputs: [],
|
||||||
|
inputTypeMismatches: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for missing connected inputs and type mismatches
|
||||||
|
connectedHandles.inputHandles.forEach((inputHandle) => {
|
||||||
|
if (!(inputHandle in newInputProps)) {
|
||||||
|
incompatibilities.missingInputs.push(inputHandle);
|
||||||
|
} else {
|
||||||
|
// Check for type mismatch on connected inputs
|
||||||
|
const currentProp = currentInputProps[inputHandle];
|
||||||
|
const newProp = newInputProps[inputHandle];
|
||||||
|
const currentType = getEffectiveType(currentProp);
|
||||||
|
const newType = getEffectiveType(newProp);
|
||||||
|
|
||||||
|
if (currentType && newType && currentType !== newType) {
|
||||||
|
incompatibilities.inputTypeMismatches.push({
|
||||||
|
name: inputHandle,
|
||||||
|
oldType: currentType,
|
||||||
|
newType: newType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for missing connected outputs
|
||||||
|
connectedHandles.outputHandles.forEach((outputHandle) => {
|
||||||
|
if (!(outputHandle in newOutputProps)) {
|
||||||
|
incompatibilities.missingOutputs.push(outputHandle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for new required inputs that didn't exist or weren't required before
|
||||||
|
newRequiredInputs.forEach((requiredInput) => {
|
||||||
|
const existedBefore = requiredInput in currentInputProps;
|
||||||
|
const wasRequiredBefore = currentRequiredInputs.includes(
|
||||||
|
requiredInput as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existedBefore || !wasRequiredBefore) {
|
||||||
|
incompatibilities.newRequiredInputs.push(requiredInput as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for new inputs that don't exist in the current version
|
||||||
|
Object.keys(newInputProps).forEach((inputName) => {
|
||||||
|
if (!(inputName in currentInputProps)) {
|
||||||
|
incompatibilities.newInputs.push(inputName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for new outputs that don't exist in the current version
|
||||||
|
Object.keys(newOutputProps).forEach((outputName) => {
|
||||||
|
if (!(outputName in currentOutputProps)) {
|
||||||
|
incompatibilities.newOutputs.push(outputName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasIncompatibilities =
|
||||||
|
incompatibilities.missingInputs.length > 0 ||
|
||||||
|
incompatibilities.missingOutputs.length > 0 ||
|
||||||
|
incompatibilities.newRequiredInputs.length > 0 ||
|
||||||
|
incompatibilities.inputTypeMismatches.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCompatible: !hasIncompatibilities,
|
||||||
|
incompatibilities: hasIncompatibilities ? incompatibilities : null,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
hasUpdate,
|
||||||
|
latestGraph,
|
||||||
|
currentInputSchema,
|
||||||
|
currentOutputSchema,
|
||||||
|
connectedHandles,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUpdate,
|
||||||
|
currentVersion: graphVersion || 0,
|
||||||
|
latestVersion: latestGraph?.version || 0,
|
||||||
|
latestGraph,
|
||||||
|
isCompatible: compatibilityResult.isCompatible,
|
||||||
|
incompatibilities: compatibilityResult.incompatibilities,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
|
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||||
|
|
||||||
interface GraphStore {
|
interface GraphStore {
|
||||||
graphExecutionStatus: AgentExecutionStatus | undefined;
|
graphExecutionStatus: AgentExecutionStatus | undefined;
|
||||||
@@ -17,6 +18,10 @@ interface GraphStore {
|
|||||||
outputSchema: Record<string, any> | null,
|
outputSchema: Record<string, any> | null,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
// Available graphs; used for sub-graph updates
|
||||||
|
availableSubGraphs: GraphMeta[];
|
||||||
|
setAvailableSubGraphs: (graphs: GraphMeta[]) => void;
|
||||||
|
|
||||||
hasInputs: () => boolean;
|
hasInputs: () => boolean;
|
||||||
hasCredentials: () => boolean;
|
hasCredentials: () => boolean;
|
||||||
hasOutputs: () => boolean;
|
hasOutputs: () => boolean;
|
||||||
@@ -29,6 +34,7 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
|||||||
inputSchema: null,
|
inputSchema: null,
|
||||||
credentialsInputSchema: null,
|
credentialsInputSchema: null,
|
||||||
outputSchema: null,
|
outputSchema: null,
|
||||||
|
availableSubGraphs: [],
|
||||||
|
|
||||||
setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => {
|
setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => {
|
||||||
set({
|
set({
|
||||||
@@ -46,6 +52,8 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
|||||||
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
|
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
|
||||||
set({ inputSchema, credentialsInputSchema, outputSchema }),
|
set({ inputSchema, credentialsInputSchema, outputSchema }),
|
||||||
|
|
||||||
|
setAvailableSubGraphs: (graphs) => set({ availableSubGraphs: graphs }),
|
||||||
|
|
||||||
hasOutputs: () => {
|
hasOutputs: () => {
|
||||||
const { outputSchema } = get();
|
const { outputSchema } = get();
|
||||||
return Object.keys(outputSchema?.properties ?? {}).length > 0;
|
return Object.keys(outputSchema?.properties ?? {}).length > 0;
|
||||||
|
|||||||
@@ -17,6 +17,25 @@ import {
|
|||||||
ensurePathExists,
|
ensurePathExists,
|
||||||
parseHandleIdToPath,
|
parseHandleIdToPath,
|
||||||
} from "@/components/renderers/InputRenderer/helpers";
|
} from "@/components/renderers/InputRenderer/helpers";
|
||||||
|
import { IncompatibilityInfo } from "../hooks/useSubAgentUpdate/types";
|
||||||
|
|
||||||
|
// Resolution mode data stored per node
|
||||||
|
export type NodeResolutionData = {
|
||||||
|
incompatibilities: IncompatibilityInfo;
|
||||||
|
// The NEW schema from the update (what we're updating TO)
|
||||||
|
pendingUpdate: {
|
||||||
|
input_schema: Record<string, unknown>;
|
||||||
|
output_schema: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
// The OLD schema before the update (what we're updating FROM)
|
||||||
|
// Needed to merge and show removed inputs during resolution
|
||||||
|
currentSchema: {
|
||||||
|
input_schema: Record<string, unknown>;
|
||||||
|
output_schema: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
// The full updated hardcoded values to apply when resolution completes
|
||||||
|
pendingHardcodedValues: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
// Minimum movement (in pixels) required before logging position change to history
|
// Minimum movement (in pixels) required before logging position change to history
|
||||||
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
||||||
@@ -65,12 +84,32 @@ type NodeStore = {
|
|||||||
backendId: string,
|
backendId: string,
|
||||||
errors: { [key: string]: string },
|
errors: { [key: string]: string },
|
||||||
) => void;
|
) => void;
|
||||||
clearAllNodeErrors: () => void; // Add this
|
|
||||||
|
|
||||||
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
|
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
|
||||||
|
|
||||||
// Credentials optional helpers
|
|
||||||
setCredentialsOptional: (nodeId: string, optional: boolean) => void;
|
setCredentialsOptional: (nodeId: string, optional: boolean) => void;
|
||||||
|
clearAllNodeErrors: () => void;
|
||||||
|
|
||||||
|
nodesInResolutionMode: Set<string>;
|
||||||
|
brokenEdgeIDs: Map<string, Set<string>>;
|
||||||
|
nodeResolutionData: Map<string, NodeResolutionData>;
|
||||||
|
setNodeResolutionMode: (
|
||||||
|
nodeID: string,
|
||||||
|
inResolution: boolean,
|
||||||
|
resolutionData?: NodeResolutionData,
|
||||||
|
) => void;
|
||||||
|
isNodeInResolutionMode: (nodeID: string) => boolean;
|
||||||
|
getNodeResolutionData: (nodeID: string) => NodeResolutionData | undefined;
|
||||||
|
setBrokenEdgeIDs: (nodeID: string, edgeIDs: string[]) => void;
|
||||||
|
removeBrokenEdgeID: (nodeID: string, edgeID: string) => void;
|
||||||
|
isEdgeBroken: (edgeID: string) => boolean;
|
||||||
|
clearResolutionState: () => void;
|
||||||
|
|
||||||
|
isInputBroken: (nodeID: string, handleID: string) => boolean;
|
||||||
|
getInputTypeMismatch: (
|
||||||
|
nodeID: string,
|
||||||
|
handleID: string,
|
||||||
|
) => string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||||
@@ -374,4 +413,99 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
|||||||
|
|
||||||
useHistoryStore.getState().pushState(newState);
|
useHistoryStore.getState().pushState(newState);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Sub-agent resolution mode state
|
||||||
|
nodesInResolutionMode: new Set<string>(),
|
||||||
|
brokenEdgeIDs: new Map<string, Set<string>>(),
|
||||||
|
nodeResolutionData: new Map<string, NodeResolutionData>(),
|
||||||
|
|
||||||
|
setNodeResolutionMode: (
|
||||||
|
nodeID: string,
|
||||||
|
inResolution: boolean,
|
||||||
|
resolutionData?: NodeResolutionData,
|
||||||
|
) => {
|
||||||
|
set((state) => {
|
||||||
|
const newNodesSet = new Set(state.nodesInResolutionMode);
|
||||||
|
const newResolutionDataMap = new Map(state.nodeResolutionData);
|
||||||
|
const newBrokenEdgeIDs = new Map(state.brokenEdgeIDs);
|
||||||
|
|
||||||
|
if (inResolution) {
|
||||||
|
newNodesSet.add(nodeID);
|
||||||
|
if (resolutionData) {
|
||||||
|
newResolutionDataMap.set(nodeID, resolutionData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newNodesSet.delete(nodeID);
|
||||||
|
newResolutionDataMap.delete(nodeID);
|
||||||
|
newBrokenEdgeIDs.delete(nodeID); // Clean up broken edges when exiting resolution mode
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodesInResolutionMode: newNodesSet,
|
||||||
|
nodeResolutionData: newResolutionDataMap,
|
||||||
|
brokenEdgeIDs: newBrokenEdgeIDs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isNodeInResolutionMode: (nodeID: string) => {
|
||||||
|
return get().nodesInResolutionMode.has(nodeID);
|
||||||
|
},
|
||||||
|
|
||||||
|
getNodeResolutionData: (nodeID: string) => {
|
||||||
|
return get().nodeResolutionData.get(nodeID);
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrokenEdgeIDs: (nodeID: string, edgeIDs: string[]) => {
|
||||||
|
set((state) => {
|
||||||
|
const newMap = new Map(state.brokenEdgeIDs);
|
||||||
|
newMap.set(nodeID, new Set(edgeIDs));
|
||||||
|
return { brokenEdgeIDs: newMap };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeBrokenEdgeID: (nodeID: string, edgeID: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const newMap = new Map(state.brokenEdgeIDs);
|
||||||
|
const nodeSet = new Set(newMap.get(nodeID) || []);
|
||||||
|
nodeSet.delete(edgeID);
|
||||||
|
newMap.set(nodeID, nodeSet);
|
||||||
|
return { brokenEdgeIDs: newMap };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdgeBroken: (edgeID: string) => {
|
||||||
|
// Check across all nodes
|
||||||
|
const brokenEdgeIDs = get().brokenEdgeIDs;
|
||||||
|
for (const edgeSet of brokenEdgeIDs.values()) {
|
||||||
|
if (edgeSet.has(edgeID)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearResolutionState: () => {
|
||||||
|
set({
|
||||||
|
nodesInResolutionMode: new Set<string>(),
|
||||||
|
brokenEdgeIDs: new Map<string, Set<string>>(),
|
||||||
|
nodeResolutionData: new Map<string, NodeResolutionData>(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper functions for input renderers
|
||||||
|
isInputBroken: (nodeID: string, handleID: string) => {
|
||||||
|
const resolutionData = get().nodeResolutionData.get(nodeID);
|
||||||
|
if (!resolutionData) return false;
|
||||||
|
return resolutionData.incompatibilities.missingInputs.includes(handleID);
|
||||||
|
},
|
||||||
|
|
||||||
|
getInputTypeMismatch: (nodeID: string, handleID: string) => {
|
||||||
|
const resolutionData = get().nodeResolutionData.get(nodeID);
|
||||||
|
if (!resolutionData) return undefined;
|
||||||
|
const mismatch = resolutionData.incompatibilities.inputTypeMismatches.find(
|
||||||
|
(m) => m.name === handleID,
|
||||||
|
);
|
||||||
|
return mismatch?.newType;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function ErrorPageContent() {
|
|||||||
) {
|
) {
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/marketplace";
|
window.document.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
href: "/profile/dashboard",
|
href: "/profile/dashboard",
|
||||||
icon: <StorefrontIcon className="size-5" />,
|
icon: <StorefrontIcon className="size-5" />,
|
||||||
},
|
},
|
||||||
...(isPaymentEnabled || true
|
...(isPaymentEnabled
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
text: "Billing",
|
text: "Billing",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const extendedButtonVariants = cva(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
|
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem] min-w-[5.5rem]",
|
||||||
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
|
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
|
||||||
icon: "p-3 !min-w-0",
|
icon: "p-3 !min-w-0",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ export const FormRenderer = ({
|
|||||||
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
|
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
|
||||||
}, [preprocessedSchema, uiSchema]);
|
}, [preprocessedSchema, uiSchema]);
|
||||||
|
|
||||||
console.log("preprocessedSchema", preprocessedSchema);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"mb-6 mt-4"}>
|
<div className={"mb-6 mt-4"}>
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { FieldProps, getUiOptions, getWidget } from "@rjsf/utils";
|
|||||||
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
|
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import { useAnyOfField } from "./useAnyOfField";
|
import { useAnyOfField } from "./useAnyOfField";
|
||||||
import { getHandleId, updateUiOption } from "../../helpers";
|
import { cleanUpHandleId, getHandleId, updateUiOption } from "../../helpers";
|
||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
import { ANY_OF_FLAG } from "../../constants";
|
import { ANY_OF_FLAG } from "../../constants";
|
||||||
import { findCustomFieldId } from "../../registry";
|
import { findCustomFieldId } from "../../registry";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export const AnyOfField = (props: FieldProps) => {
|
export const AnyOfField = (props: FieldProps) => {
|
||||||
const { registry, schema } = props;
|
const { registry, schema } = props;
|
||||||
@@ -21,6 +23,8 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
field_id,
|
field_id,
|
||||||
} = useAnyOfField(props);
|
} = useAnyOfField(props);
|
||||||
|
|
||||||
|
const isInputBroken = useNodeStore((state) => state.isInputBroken);
|
||||||
|
|
||||||
const parentCustomFieldId = findCustomFieldId(schema);
|
const parentCustomFieldId = findCustomFieldId(schema);
|
||||||
if (parentCustomFieldId) {
|
if (parentCustomFieldId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -43,6 +47,7 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||||
|
const isAnyOfInputBroken = isInputBroken(nodeId, cleanUpHandleId(handleId));
|
||||||
|
|
||||||
// Now anyOf can render - custom fields if the option schema matches a custom field
|
// Now anyOf can render - custom fields if the option schema matches a custom field
|
||||||
const optionCustomFieldId = optionSchema
|
const optionCustomFieldId = optionSchema
|
||||||
@@ -78,7 +83,11 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
registry={registry}
|
registry={registry}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
autocomplete={props.autocomplete}
|
autocomplete={props.autocomplete}
|
||||||
className="-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium"
|
className={cn(
|
||||||
|
"-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium",
|
||||||
|
isAnyOfInputBroken &&
|
||||||
|
"border-red-500 bg-red-100 text-red-600 line-through",
|
||||||
|
)}
|
||||||
autofocus={props.autofocus}
|
autofocus={props.autofocus}
|
||||||
label=""
|
label=""
|
||||||
hideLabel={true}
|
hideLabel={true}
|
||||||
@@ -93,7 +102,7 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
selector={selector}
|
selector={selector}
|
||||||
uiSchema={updatedUiSchema}
|
uiSchema={updatedUiSchema}
|
||||||
/>
|
/>
|
||||||
{!isHandleConnected && optionsSchemaField}
|
{!isHandleConnected && !isAnyOfInputBroken && optionsSchemaField}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Text } from "@/components/atoms/Text/Text";
|
|||||||
import { isOptionalType } from "../../../utils/schema-utils";
|
import { isOptionalType } from "../../../utils/schema-utils";
|
||||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
|
||||||
interface customFieldProps extends FieldProps {
|
interface customFieldProps extends FieldProps {
|
||||||
selector: JSX.Element;
|
selector: JSX.Element;
|
||||||
@@ -51,6 +52,13 @@ export const AnyOfFieldTitle = (props: customFieldProps) => {
|
|||||||
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
|
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
|
||||||
const shoudlShowType = isHandleConnected || (isOptional && type);
|
const shoudlShowType = isHandleConnected || (isOptional && type);
|
||||||
|
|
||||||
|
const isInputBroken = useNodeStore((state) =>
|
||||||
|
state.isInputBroken(nodeId, cleanUpHandleId(uiOptions.handleId)),
|
||||||
|
);
|
||||||
|
const inputMismatch = useNodeStore((state) =>
|
||||||
|
state.getInputTypeMismatch(nodeId, cleanUpHandleId(uiOptions.handleId)),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TitleFieldTemplate
|
<TitleFieldTemplate
|
||||||
@@ -62,8 +70,16 @@ export const AnyOfFieldTitle = (props: customFieldProps) => {
|
|||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
/>
|
/>
|
||||||
{shoudlShowType && (
|
{shoudlShowType && (
|
||||||
<Text variant="small" className={cn("text-zinc-700", colorClass)}>
|
<Text
|
||||||
{isOptional ? `(${displayType})` : "(any)"}
|
variant="small"
|
||||||
|
className={cn(
|
||||||
|
"text-zinc-700",
|
||||||
|
isInputBroken && "line-through",
|
||||||
|
colorClass,
|
||||||
|
inputMismatch && "rounded-md bg-red-100 px-1 !text-red-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isOptional ? `(${inputMismatch || displayType})` : "(any)"}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{shouldShowSelector && selector}
|
{shouldShowSelector && selector}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { Text } from "@/components/atoms/Text/Text";
|
|||||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||||
import { isAnyOfSchema } from "../../utils/schema-utils";
|
import { isAnyOfSchema } from "../../utils/schema-utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { isArrayItem } from "../../helpers";
|
import { cleanUpHandleId, isArrayItem } from "../../helpers";
|
||||||
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
|
||||||
export default function TitleField(props: TitleFieldProps) {
|
export default function TitleField(props: TitleFieldProps) {
|
||||||
const { id, title, required, schema, registry, uiSchema } = props;
|
const { id, title, required, schema, registry, uiSchema } = props;
|
||||||
@@ -26,6 +27,11 @@ export default function TitleField(props: TitleFieldProps) {
|
|||||||
const smallText = isArrayItemFlag || additional;
|
const smallText = isArrayItemFlag || additional;
|
||||||
|
|
||||||
const showHandle = uiOptions.showHandles ?? showHandles;
|
const showHandle = uiOptions.showHandles ?? showHandles;
|
||||||
|
|
||||||
|
const isInputBroken = useNodeStore((state) =>
|
||||||
|
state.isInputBroken(nodeId, cleanUpHandleId(uiOptions.handleId)),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{showHandle !== false && (
|
{showHandle !== false && (
|
||||||
@@ -34,7 +40,11 @@ export default function TitleField(props: TitleFieldProps) {
|
|||||||
<Text
|
<Text
|
||||||
variant={isArrayItemFlag ? "small" : "body"}
|
variant={isArrayItemFlag ? "small" : "body"}
|
||||||
id={id}
|
id={id}
|
||||||
className={cn("line-clamp-1", smallText && "text-sm text-zinc-700")}
|
className={cn(
|
||||||
|
"line-clamp-1",
|
||||||
|
smallText && "text-sm text-zinc-700",
|
||||||
|
isInputBroken && "text-red-500 line-through",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -44,7 +54,7 @@ export default function TitleField(props: TitleFieldProps) {
|
|||||||
{!isAnyOf && (
|
{!isAnyOf && (
|
||||||
<Text
|
<Text
|
||||||
variant="small"
|
variant="small"
|
||||||
className={cn("ml-2", colorClass)}
|
className={cn("ml-2", isInputBroken && "line-through", colorClass)}
|
||||||
id={description_id}
|
id={description_id}
|
||||||
>
|
>
|
||||||
({displayType})
|
({displayType})
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export function updateUiOption<T extends Record<string, any>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cleanUpHandleId = (handleId: string) => {
|
export const cleanUpHandleId = (handleId: string) => {
|
||||||
|
if (!handleId) return "";
|
||||||
|
|
||||||
let newHandleId = handleId;
|
let newHandleId = handleId;
|
||||||
if (handleId.includes(ANY_OF_FLAG)) {
|
if (handleId.includes(ANY_OF_FLAG)) {
|
||||||
newHandleId = newHandleId.replace(ANY_OF_FLAG, "");
|
newHandleId = newHandleId.replace(ANY_OF_FLAG, "");
|
||||||
|
|||||||
@@ -233,13 +233,14 @@ export default function useAgentGraph(
|
|||||||
title: `${block.name} ${node.id}`,
|
title: `${block.name} ${node.id}`,
|
||||||
inputSchema: block.inputSchema,
|
inputSchema: block.inputSchema,
|
||||||
outputSchema: block.outputSchema,
|
outputSchema: block.outputSchema,
|
||||||
|
isOutputStatic: block.staticOutput,
|
||||||
hardcodedValues: node.input_default,
|
hardcodedValues: node.input_default,
|
||||||
uiType: block.uiType,
|
uiType: block.uiType,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
connections: graph.links
|
connections: graph.links
|
||||||
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
|
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
|
||||||
.map((link) => ({
|
.map((link) => ({
|
||||||
edge_id: formatEdgeID(link),
|
id: formatEdgeID(link),
|
||||||
source: link.source_id,
|
source: link.source_id,
|
||||||
sourceHandle: link.source_name,
|
sourceHandle: link.source_name,
|
||||||
target: link.sink_id,
|
target: link.sink_id,
|
||||||
|
|||||||
@@ -245,8 +245,8 @@ export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
|
|||||||
// At the time of writing, combined schemas only occur on the first nested level in a
|
// At the time of writing, combined schemas only occur on the first nested level in a
|
||||||
// block schema. It is typed this way to make the use of these objects less tedious.
|
// block schema. It is typed this way to make the use of these objects less tedious.
|
||||||
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & {
|
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & {
|
||||||
type: never;
|
type?: never;
|
||||||
const: never;
|
const?: never;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
allOf: [BlockIOSimpleTypeSubSchema];
|
allOf: [BlockIOSimpleTypeSubSchema];
|
||||||
@@ -368,8 +368,8 @@ export type GraphMeta = {
|
|||||||
recommended_schedule_cron: string | null;
|
recommended_schedule_cron: string | null;
|
||||||
forked_from_id?: GraphID | null;
|
forked_from_id?: GraphID | null;
|
||||||
forked_from_version?: number | null;
|
forked_from_version?: number | null;
|
||||||
input_schema: GraphIOSchema;
|
input_schema: GraphInputSchema;
|
||||||
output_schema: GraphIOSchema;
|
output_schema: GraphOutputSchema;
|
||||||
credentials_input_schema: CredentialsInputSchema;
|
credentials_input_schema: CredentialsInputSchema;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
@@ -385,19 +385,51 @@ export type GraphMeta = {
|
|||||||
export type GraphID = Brand<string, "GraphID">;
|
export type GraphID = Brand<string, "GraphID">;
|
||||||
|
|
||||||
/* Derived from backend/data/graph.py:Graph._generate_schema() */
|
/* Derived from backend/data/graph.py:Graph._generate_schema() */
|
||||||
export type GraphIOSchema = {
|
export type GraphInputSchema = {
|
||||||
type: "object";
|
type: "object";
|
||||||
properties: Record<string, GraphIOSubSchema>;
|
properties: Record<string, GraphInputSubSchema>;
|
||||||
required: (keyof BlockIORootSchema["properties"])[];
|
required: (keyof GraphInputSchema["properties"])[];
|
||||||
};
|
};
|
||||||
export type GraphIOSubSchema = Omit<
|
export type GraphInputSubSchema = GraphOutputSubSchema &
|
||||||
BlockIOSubSchemaMeta,
|
(
|
||||||
"placeholder" | "depends_on" | "hidden"
|
| { type?: never; default: any | null } // AgentInputBlock (generic Any type)
|
||||||
> & {
|
| { type: "string"; format: "short-text"; default: string | null } // AgentShortTextInputBlock
|
||||||
type: never; // bodge to avoid type checking hell; doesn't exist at runtime
|
| { type: "string"; format: "long-text"; default: string | null } // AgentLongTextInputBlock
|
||||||
default?: string;
|
| { type: "integer"; default: number | null } // AgentNumberInputBlock
|
||||||
|
| { type: "string"; format: "date"; default: string | null } // AgentDateInputBlock
|
||||||
|
| { type: "string"; format: "time"; default: string | null } // AgentTimeInputBlock
|
||||||
|
| { type: "string"; format: "file"; default: string | null } // AgentFileInputBlock
|
||||||
|
| { type: "string"; enum: string[]; default: string | null } // AgentDropdownInputBlock
|
||||||
|
| { type: "boolean"; default: boolean } // AgentToggleInputBlock
|
||||||
|
| {
|
||||||
|
// AgentTableInputBlock
|
||||||
|
type: "array";
|
||||||
|
format: "table";
|
||||||
|
items: {
|
||||||
|
type: "object";
|
||||||
|
properties: Record<string, { type: "string" }>;
|
||||||
|
};
|
||||||
|
default: Array<Record<string, string>> | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// AgentGoogleDriveFileInputBlock
|
||||||
|
type: "object";
|
||||||
|
format: "google-drive-picker";
|
||||||
|
google_drive_picker_config?: GoogleDrivePickerConfig;
|
||||||
|
default: GoogleDriveFile | null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export type GraphOutputSchema = {
|
||||||
|
type: "object";
|
||||||
|
properties: Record<string, GraphOutputSubSchema>;
|
||||||
|
required: (keyof GraphOutputSchema["properties"])[];
|
||||||
|
};
|
||||||
|
export type GraphOutputSubSchema = {
|
||||||
|
// TODO: typed outputs based on the incoming edges?
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
advanced: boolean;
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
metadata?: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CredentialsInputSchema = {
|
export type CredentialsInputSchema = {
|
||||||
@@ -440,8 +472,8 @@ export type GraphUpdateable = Omit<
|
|||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
nodes: NodeCreatable[];
|
nodes: NodeCreatable[];
|
||||||
links: LinkCreatable[];
|
links: LinkCreatable[];
|
||||||
input_schema?: GraphIOSchema;
|
input_schema?: GraphInputSchema;
|
||||||
output_schema?: GraphIOSchema;
|
output_schema?: GraphOutputSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphCreatable = _GraphCreatableInner & {
|
export type GraphCreatable = _GraphCreatableInner & {
|
||||||
@@ -497,8 +529,8 @@ export type LibraryAgent = {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
instructions?: string | null;
|
instructions?: string | null;
|
||||||
input_schema: GraphIOSchema;
|
input_schema: GraphInputSchema;
|
||||||
output_schema: GraphIOSchema;
|
output_schema: GraphOutputSchema;
|
||||||
credentials_input_schema: CredentialsInputSchema;
|
credentials_input_schema: CredentialsInputSchema;
|
||||||
new_output: boolean;
|
new_output: boolean;
|
||||||
can_access_graph: boolean;
|
can_access_graph: boolean;
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { NodeDimension } from "@/app/(platform)/build/components/legacy-builder/
|
|||||||
import {
|
import {
|
||||||
BlockIOObjectSubSchema,
|
BlockIOObjectSubSchema,
|
||||||
BlockIORootSchema,
|
BlockIORootSchema,
|
||||||
|
BlockIOSubSchema,
|
||||||
Category,
|
Category,
|
||||||
|
GraphInputSubSchema,
|
||||||
|
GraphOutputSubSchema,
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
@@ -76,8 +79,8 @@ export function getTypeBgColor(type: string | null): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeColor(type: string | null): string {
|
export function getTypeColor(type: string | undefined): string {
|
||||||
if (type === null) return "#6b7280";
|
if (!type) return "#6b7280";
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
string: "#22c55e",
|
string: "#22c55e",
|
||||||
@@ -88,11 +91,59 @@ export function getTypeColor(type: string | null): string {
|
|||||||
array: "#6366f1",
|
array: "#6366f1",
|
||||||
null: "#6b7280",
|
null: "#6b7280",
|
||||||
any: "#6b7280",
|
any: "#6b7280",
|
||||||
"": "#6b7280",
|
|
||||||
}[type] || "#6b7280"
|
}[type] || "#6b7280"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the effective type from a JSON schema, handling anyOf/oneOf/allOf wrappers.
|
||||||
|
* Returns the first non-null type found in the schema structure.
|
||||||
|
*/
|
||||||
|
export function getEffectiveType(
|
||||||
|
schema:
|
||||||
|
| BlockIOSubSchema
|
||||||
|
| GraphInputSubSchema
|
||||||
|
| GraphOutputSubSchema
|
||||||
|
| null
|
||||||
|
| undefined,
|
||||||
|
): string | undefined {
|
||||||
|
if (!schema) return undefined;
|
||||||
|
|
||||||
|
// Direct type property
|
||||||
|
if ("type" in schema && schema.type) {
|
||||||
|
return String(schema.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle allOf - typically a single-item wrapper
|
||||||
|
if (
|
||||||
|
"allOf" in schema &&
|
||||||
|
Array.isArray(schema.allOf) &&
|
||||||
|
schema.allOf.length > 0
|
||||||
|
) {
|
||||||
|
return getEffectiveType(schema.allOf[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle anyOf - e.g. [{ type: "string" }, { type: "null" }]
|
||||||
|
if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
|
||||||
|
for (const item of schema.anyOf) {
|
||||||
|
if ("type" in item && item.type !== "null") {
|
||||||
|
return String(item.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle oneOf
|
||||||
|
if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
|
||||||
|
for (const item of schema.oneOf) {
|
||||||
|
if ("type" in item && item.type !== "null") {
|
||||||
|
return String(item.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function beautifyString(name: string): string {
|
export function beautifyString(name: string): string {
|
||||||
// Regular expression to identify places to split, considering acronyms
|
// Regular expression to identify places to split, considering acronyms
|
||||||
const result = name
|
const result = name
|
||||||
|
|||||||
Reference in New Issue
Block a user