mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-07 22:33:57 -05:00
implement in new builder
This commit is contained in:
@@ -57,18 +57,17 @@ export const RunInputDialog = ({
|
||||
Credentials
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<FormRenderer
|
||||
jsonSchema={credentialsSchema as RJSFSchema}
|
||||
handleChange={(v) => handleCredentialChange(v.formData)}
|
||||
uiSchema={credentialsUiSchema}
|
||||
initialValues={{}}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormRenderer
|
||||
className="mt-4 px-2"
|
||||
jsonSchema={credentialsSchema as RJSFSchema}
|
||||
handleChange={(v) => handleCredentialChange(v.formData)}
|
||||
uiSchema={credentialsUiSchema}
|
||||
initialValues={{}}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -80,18 +79,17 @@ export const RunInputDialog = ({
|
||||
Inputs
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<FormRenderer
|
||||
jsonSchema={inputSchema as RJSFSchema}
|
||||
handleChange={(v) => handleInputChange(v.formData)}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={{}}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormRenderer
|
||||
className="mt-4 px-2"
|
||||
jsonSchema={inputSchema as RJSFSchema}
|
||||
handleChange={(v) => handleInputChange(v.formData)}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={{}}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/def
|
||||
import {
|
||||
useGetV1GetExecutionDetails,
|
||||
useGetV1GetSpecificGraph,
|
||||
useGetV1ListUserGraphs,
|
||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
@@ -17,6 +18,7 @@ import { useReactFlow } from "@xyflow/react";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { useHistoryStore } from "../../../stores/historyStore";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
|
||||
export const useFlow = () => {
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
@@ -35,6 +37,9 @@ export const useFlow = () => {
|
||||
const setGraphExecutionStatus = useGraphStore(
|
||||
useShallow((state) => state.setGraphExecutionStatus),
|
||||
);
|
||||
const setAvailableSubGraphs = useGraphStore(
|
||||
useShallow((state) => state.setAvailableSubGraphs),
|
||||
);
|
||||
const updateEdgeBeads = useEdgeStore(
|
||||
useShallow((state) => state.updateEdgeBeads),
|
||||
);
|
||||
@@ -61,6 +66,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(
|
||||
flowID ?? "",
|
||||
flowVersion !== null ? { version: flowVersion } : {},
|
||||
@@ -115,6 +125,13 @@ export const useFlow = () => {
|
||||
}
|
||||
}, [graph]);
|
||||
|
||||
// Update available sub-graphs in store for sub-agent update detection
|
||||
useEffect(() => {
|
||||
if (availableGraphs) {
|
||||
setAvailableSubGraphs(availableGraphs);
|
||||
}
|
||||
}, [availableGraphs, setAvailableSubGraphs]);
|
||||
|
||||
// adding nodes
|
||||
useEffect(() => {
|
||||
if (customNodes.length > 0) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getBezierPath,
|
||||
} from "@xyflow/react";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { XIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
|
||||
@@ -35,6 +36,9 @@ const CustomEdge = ({
|
||||
selected,
|
||||
}: EdgeProps<CustomEdge>) => {
|
||||
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
||||
// Subscribe to the actual brokenEdgeIds state so we re-render when it changes
|
||||
const isBroken = useNodeStore((state) => state.brokenEdgeIDs.has(id));
|
||||
const removeBrokenEdgeID = useNodeStore((state) => state.removeBrokenEdgeID);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
@@ -50,6 +54,14 @@ const CustomEdge = ({
|
||||
const beadUp = data?.beadUp ?? 0;
|
||||
const beadDown = data?.beadDown ?? 0;
|
||||
|
||||
const handleRemoveEdge = () => {
|
||||
removeConnection(id);
|
||||
// Also remove from broken edges tracking if it was broken
|
||||
if (isBroken) {
|
||||
removeBrokenEdgeID(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
@@ -57,9 +69,11 @@ const CustomEdge = ({
|
||||
markerEnd={markerEnd}
|
||||
className={cn(
|
||||
isStatic && "!stroke-[1.5px] [stroke-dasharray:6]",
|
||||
selected
|
||||
? "stroke-zinc-800"
|
||||
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
||||
isBroken
|
||||
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
||||
: selected
|
||||
? "stroke-zinc-800"
|
||||
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
||||
)}
|
||||
/>
|
||||
<JSBeads
|
||||
@@ -70,12 +84,16 @@ const CustomEdge = ({
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
<Button
|
||||
onClick={() => removeConnection(id)}
|
||||
onClick={handleRemoveEdge}
|
||||
className={cn(
|
||||
"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={{
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: "all",
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
import { CircleIcon } from "@phosphor-icons/react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type NodeHandleProps = {
|
||||
handleId: string;
|
||||
isConnected: boolean;
|
||||
side: "left" | "right";
|
||||
isBroken?: boolean;
|
||||
};
|
||||
|
||||
const NodeHandle = ({
|
||||
handleId,
|
||||
isConnected,
|
||||
side,
|
||||
}: {
|
||||
handleId: string;
|
||||
isConnected: boolean;
|
||||
side: "left" | "right";
|
||||
}) => {
|
||||
isBroken = false,
|
||||
}: NodeHandleProps) => {
|
||||
return (
|
||||
<Handle
|
||||
type={side === "left" ? "target" : "source"}
|
||||
position={side === "left" ? Position.Left : Position.Right}
|
||||
id={handleId}
|
||||
className={side === "left" ? "-ml-4 mr-2" : "-mr-2 ml-2"}
|
||||
className={cn(
|
||||
side === "left" ? "-ml-4 mr-2" : "-mr-2 ml-2",
|
||||
isBroken && "pointer-events-none",
|
||||
)}
|
||||
isConnectable={!isBroken}
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<CircleIcon
|
||||
size={16}
|
||||
weight={isConnected ? "fill" : "duotone"}
|
||||
className={"text-gray-400 opacity-100"}
|
||||
className={cn(
|
||||
"opacity-100",
|
||||
isBroken ? "text-red-500" : "text-gray-400",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Handle>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Node as XYNode, NodeProps } from "@xyflow/react";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { BlockUIType } from "../../../types";
|
||||
@@ -19,6 +19,55 @@ import { cn } from "@/lib/utils";
|
||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||
import { AyrshareConnectButton } from "./components/AyrshareConnectButton";
|
||||
import { NodeModelMetadata } from "@/app/api/__generated__/models/nodeModelMetadata";
|
||||
import { SubAgentUpdateFeature } from "./components/SubAgentUpdate/SubAgentUpdateFeature";
|
||||
import { useNodeStore, NodeResolutionData } from "../../../../stores/nodeStore";
|
||||
|
||||
type SchemaProperties = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Merges schemas during resolution mode to include removed inputs/outputs
|
||||
* that still have connections, so users can see and delete them.
|
||||
*/
|
||||
function mergeSchemaForResolution(
|
||||
currentSchema: Record<string, unknown>,
|
||||
newSchema: Record<string, unknown>,
|
||||
resolutionData: NodeResolutionData,
|
||||
type: "input" | "output",
|
||||
): Record<string, unknown> {
|
||||
const newProps = (newSchema.properties as SchemaProperties) || {};
|
||||
const currentProps = (currentSchema.properties as SchemaProperties) || {};
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export type CustomNodeData = {
|
||||
hardcodedValues: {
|
||||
@@ -44,6 +93,53 @@ export type CustomNode = XYNode<CustomNodeData, "custom">;
|
||||
|
||||
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
({ data, id: nodeId, selected }) => {
|
||||
// Subscribe to the actual resolution mode state for this node
|
||||
const isInResolutionMode = useNodeStore((state) =>
|
||||
state.nodesInResolutionMode.has(nodeId),
|
||||
);
|
||||
const resolutionData = useNodeStore((state) =>
|
||||
state.nodeResolutionData.get(nodeId),
|
||||
);
|
||||
|
||||
const isAgent = data.uiType === BlockUIType.AGENT;
|
||||
|
||||
// Get base schemas for agent nodes
|
||||
const currentInputSchema = isAgent
|
||||
? (data.hardcodedValues.input_schema ?? {})
|
||||
: data.inputSchema;
|
||||
const currentOutputSchema = isAgent
|
||||
? (data.hardcodedValues.output_schema ?? {})
|
||||
: data.outputSchema;
|
||||
|
||||
// During resolution mode, merge old connected inputs/outputs with new schema
|
||||
// so users can see and delete the broken connections
|
||||
const inputSchema = useMemo(() => {
|
||||
if (isAgent && isInResolutionMode && resolutionData) {
|
||||
// Use the stored old schema from resolution data for merging
|
||||
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) {
|
||||
// Use the stored old schema from resolution data for merging
|
||||
return mergeSchemaForResolution(
|
||||
resolutionData.currentSchema.output_schema,
|
||||
resolutionData.pendingUpdate.output_schema,
|
||||
resolutionData,
|
||||
"output",
|
||||
);
|
||||
}
|
||||
return currentOutputSchema;
|
||||
}, [isAgent, isInResolutionMode, resolutionData, currentOutputSchema]);
|
||||
|
||||
// Handle sticky note separately
|
||||
if (data.uiType === BlockUIType.NOTE) {
|
||||
return (
|
||||
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
|
||||
@@ -62,16 +158,6 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
|
||||
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 =
|
||||
data.errors &&
|
||||
Object.values(data.errors).some(
|
||||
@@ -92,6 +178,7 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
|
||||
<div className="rounded-xlarge bg-white">
|
||||
<NodeHeader data={data} nodeId={nodeId} />
|
||||
{isAgent && <SubAgentUpdateFeature nodeID={nodeId} nodeData={data} />}
|
||||
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
|
||||
{isAyrshare && <AyrshareConnectButton />}
|
||||
<FormCreator
|
||||
@@ -99,7 +186,7 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
className={cn(
|
||||
"bg-white pr-6",
|
||||
"my-4 bg-white pr-6",
|
||||
isWebhook && "pointer-events-none opacity-50",
|
||||
)}
|
||||
showHandles={showHandles}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import React from "react";
|
||||
import {
|
||||
WarningIcon,
|
||||
XCircleIcon,
|
||||
PlusCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate";
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="rounded-md bg-amber-50 p-3 text-sm text-amber-800 dark:bg-amber-900/30 dark:text-amber-200">
|
||||
<p>
|
||||
If you proceed, you'll need to remove the broken connections
|
||||
before you can save or run your agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="ghost" size="small" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onConfirm}
|
||||
className="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";
|
||||
|
||||
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,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 "./IncompatibleUpdateDialog";
|
||||
import { ResolutionModeBar } from "./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,208 @@
|
||||
import { useState, useCallback, useMemo, 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,
|
||||
createUpdatedHardcodedValues,
|
||||
getBrokenEdgeIDs,
|
||||
GraphMetaLike,
|
||||
} from "@/app/(platform)/build/hooks/useSubAgentUpdate";
|
||||
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
||||
import { CustomNodeData } from "../../CustomNode";
|
||||
|
||||
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),
|
||||
);
|
||||
const brokenEdgeIds = useNodeStore(
|
||||
useShallow((state) => state.brokenEdgeIDs),
|
||||
);
|
||||
const getNodeResolutionData = useNodeStore(
|
||||
useShallow((state) => state.getNodeResolutionData),
|
||||
);
|
||||
const edges = useEdgeStore(useShallow((state) => state.edges));
|
||||
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;
|
||||
|
||||
// Get node connections
|
||||
const nodeConnections = useMemo(() => {
|
||||
return edges.filter(
|
||||
(edge) => edge.source === nodeID || edge.target === nodeID,
|
||||
);
|
||||
}, [edges, nodeID]);
|
||||
|
||||
// Use the sub-agent update hook
|
||||
const updateInfo = useSubAgentUpdate(
|
||||
nodeID,
|
||||
graphID,
|
||||
graphVersion,
|
||||
currentInputSchema,
|
||||
currentOutputSchema,
|
||||
nodeConnections,
|
||||
availableSubGraphs,
|
||||
);
|
||||
|
||||
const isInResolutionMode = isNodeInResolutionMode(nodeID);
|
||||
|
||||
// Handle update button click
|
||||
const handleUpdateClick = useCallback(() => {
|
||||
if (!updateInfo.hasUpdate || !updateInfo.latestFlow) return;
|
||||
|
||||
if (updateInfo.isCompatible) {
|
||||
// Compatible update - apply directly
|
||||
const newHardcodedValues = createUpdatedHardcodedValues(
|
||||
nodeData.hardcodedValues,
|
||||
updateInfo.latestFlow as GraphMetaLike,
|
||||
);
|
||||
updateNodeData(nodeID, { hardcodedValues: newHardcodedValues });
|
||||
} else {
|
||||
// Incompatible update - show dialog
|
||||
setShowIncompatibilityDialog(true);
|
||||
}
|
||||
}, [
|
||||
updateInfo.hasUpdate,
|
||||
updateInfo.latestFlow,
|
||||
updateInfo.isCompatible,
|
||||
nodeData.hardcodedValues,
|
||||
updateNodeData,
|
||||
nodeID,
|
||||
]);
|
||||
|
||||
// Handle confirming an incompatible update
|
||||
const handleConfirmIncompatibleUpdate = useCallback(() => {
|
||||
if (!updateInfo.latestFlow || !updateInfo.incompatibilities) return;
|
||||
|
||||
const latestFlow = updateInfo.latestFlow as GraphMetaLike;
|
||||
|
||||
// Get the new schemas from the latest flow
|
||||
const newInputSchema =
|
||||
(latestFlow.input_schema as Record<string, unknown>) || {};
|
||||
const newOutputSchema =
|
||||
(latestFlow.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 = createUpdatedHardcodedValues(
|
||||
nodeData.hardcodedValues,
|
||||
latestFlow,
|
||||
);
|
||||
|
||||
// Get broken edge IDs
|
||||
const brokenIds = getBrokenEdgeIDs(
|
||||
nodeConnections,
|
||||
updateInfo.incompatibilities,
|
||||
nodeID,
|
||||
);
|
||||
setBrokenEdgeIds(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);
|
||||
}, [
|
||||
updateInfo.latestFlow,
|
||||
updateInfo.incompatibilities,
|
||||
nodeData.hardcodedValues,
|
||||
currentInputSchema,
|
||||
currentOutputSchema,
|
||||
nodeID,
|
||||
nodeConnections,
|
||||
setBrokenEdgeIds,
|
||||
setNodeResolutionMode,
|
||||
]);
|
||||
|
||||
// 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) =>
|
||||
edges.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(nodeID, false);
|
||||
setBrokenEdgeIds([]);
|
||||
}
|
||||
}, [
|
||||
isInResolutionMode,
|
||||
brokenEdgeIds,
|
||||
edges,
|
||||
resolutionData,
|
||||
updateNodeData,
|
||||
setNodeResolutionMode,
|
||||
nodeID,
|
||||
setBrokenEdgeIds,
|
||||
]);
|
||||
|
||||
return {
|
||||
updateInfo,
|
||||
isInResolutionMode,
|
||||
resolutionData,
|
||||
showIncompatibilityDialog,
|
||||
setShowIncompatibilityDialog,
|
||||
handleUpdateClick,
|
||||
handleConfirmIncompatibleUpdate,
|
||||
};
|
||||
}
|
||||
@@ -1,30 +1,52 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { uiSchema } from "./uiSchema";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { BlockUIType } from "../../types";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
|
||||
export const FormCreator = React.memo(
|
||||
({
|
||||
jsonSchema,
|
||||
nodeId,
|
||||
uiType,
|
||||
showHandles = true,
|
||||
className,
|
||||
}: {
|
||||
jsonSchema: RJSFSchema;
|
||||
nodeId: string;
|
||||
uiType: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
interface FormCreatorProps {
|
||||
jsonSchema: RJSFSchema;
|
||||
nodeId: string;
|
||||
uiType: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FormCreator: React.FC<FormCreatorProps> = React.memo(
|
||||
({ jsonSchema, nodeId, uiType, showHandles = true, className }) => {
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
|
||||
const getHardCodedValues = useNodeStore(
|
||||
(state) => state.getHardCodedValues,
|
||||
);
|
||||
|
||||
// Subscribe to resolution mode state to get broken inputs
|
||||
const resolutionData = useNodeStore((state) =>
|
||||
state.nodeResolutionData.get(nodeId),
|
||||
);
|
||||
|
||||
// Compute the set of broken input handles (only missing inputs, not type mismatches)
|
||||
// Type mismatches still have a valid handle - only the edge is broken, not the handle itself
|
||||
const brokenInputs = useMemo(() => {
|
||||
if (!resolutionData) return new Set<string>();
|
||||
const broken = new Set<string>();
|
||||
resolutionData.incompatibilities.missingInputs.forEach((name) =>
|
||||
broken.add(name),
|
||||
);
|
||||
return broken;
|
||||
}, [resolutionData]);
|
||||
|
||||
// Compute a map of inputs with type mismatches -> their new type (for highlighting and display)
|
||||
const typeMismatchInputs = useMemo(() => {
|
||||
if (!resolutionData) return new Map<string, string>();
|
||||
const mismatches = new Map<string, string>();
|
||||
resolutionData.incompatibilities.inputTypeMismatches.forEach((m) =>
|
||||
mismatches.set(m.name, m.newType),
|
||||
);
|
||||
return mismatches;
|
||||
}, [resolutionData]);
|
||||
|
||||
const handleChange = ({ formData }: any) => {
|
||||
if ("credentials" in formData && !formData.credentials?.id) {
|
||||
delete formData.credentials;
|
||||
@@ -48,20 +70,21 @@ export const FormCreator = React.memo(
|
||||
: hardcodedValues;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<FormRenderer
|
||||
jsonSchema={jsonSchema}
|
||||
handleChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={initialValues}
|
||||
formContext={{
|
||||
nodeId: nodeId,
|
||||
uiType: uiType,
|
||||
showHandles: showHandles,
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormRenderer
|
||||
className={className}
|
||||
jsonSchema={jsonSchema}
|
||||
handleChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={initialValues}
|
||||
formContext={{
|
||||
nodeId: nodeId,
|
||||
uiType: uiType,
|
||||
showHandles: showHandles,
|
||||
size: "small",
|
||||
brokenInputs: brokenInputs,
|
||||
typeMismatchInputs: typeMismatchInputs,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CaretDownIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import NodeHandle from "../handlers/NodeHandle";
|
||||
import {
|
||||
@@ -12,9 +12,32 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { getTypeDisplayInfo } from "./helpers";
|
||||
import { generateHandleId } from "../handlers/helpers";
|
||||
import { BlockUIType } from "../../types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Hook to get the set of broken output names for a node in resolution mode.
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
|
||||
export const OutputHandler = ({
|
||||
outputSchema,
|
||||
@@ -28,6 +51,7 @@ export const OutputHandler = ({
|
||||
const { isOutputConnected } = useEdgeStore();
|
||||
const properties = outputSchema?.properties || {};
|
||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||
const brokenOutputs = useBrokenOutputs(nodeId);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xlarge border-t border-slate-200/50 bg-white py-3.5">
|
||||
@@ -53,6 +77,7 @@ export const OutputHandler = ({
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{Object.entries(properties).map(([key, property]: [string, any]) => {
|
||||
const isConnected = isOutputConnected(nodeId, key);
|
||||
const isBroken = brokenOutputs.has(key);
|
||||
const shouldShow = isConnected || isOutputVisible;
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(property);
|
||||
|
||||
@@ -74,7 +99,13 @@ export const OutputHandler = ({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Text variant="body" className="text-slate-700">
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn(
|
||||
"text-slate-700",
|
||||
isBroken && "text-red-500 line-through",
|
||||
)}
|
||||
>
|
||||
{property?.title || key}{" "}
|
||||
</Text>
|
||||
<Text variant="small" as="span" className={colorClass}>
|
||||
@@ -87,6 +118,7 @@ export const OutputHandler = ({
|
||||
}
|
||||
isConnected={isConnected}
|
||||
side="right"
|
||||
isBroken={isBroken}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getTypeTextColor,
|
||||
getEffectiveType,
|
||||
} from "@/lib/utils";
|
||||
import { FC, memo, useCallback, useMemo } from "react";
|
||||
import { FC, memo, useCallback } from "react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
|
||||
|
||||
@@ -1,12 +1,57 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
GraphMeta,
|
||||
GraphMeta as LegacyGraphMeta,
|
||||
GraphInputSchema,
|
||||
GraphOutputSchema,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { GraphMeta as GeneratedGraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||
import { getEffectiveType } from "@/lib/utils";
|
||||
import { ConnectionData } from "../components/legacy-builder/CustomNode/CustomNode";
|
||||
|
||||
// Union type for GraphMeta that works with both legacy and new builder
|
||||
export type GraphMetaLike = LegacyGraphMeta | GeneratedGraphMeta;
|
||||
|
||||
// Generic edge type that works with both builders
|
||||
// Legacy builder uses ConnectionData with `edge_id`, new builder uses CustomEdge with `id`
|
||||
export type EdgeLike = {
|
||||
id?: string;
|
||||
edge_id?: string;
|
||||
source: string;
|
||||
target: string;
|
||||
sourceHandle?: string | null;
|
||||
targetHandle?: string | null;
|
||||
};
|
||||
|
||||
// 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
|
||||
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 {};
|
||||
}
|
||||
|
||||
function getSchemaRequired(schema: unknown): SchemaRequired {
|
||||
if (
|
||||
schema &&
|
||||
typeof schema === "object" &&
|
||||
"required" in schema &&
|
||||
Array.isArray(schema.required)
|
||||
) {
|
||||
return schema.required as SchemaRequired;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export type IncompatibilityInfo = {
|
||||
missingInputs: string[]; // Connected inputs that no longer exist
|
||||
missingOutputs: string[]; // Connected outputs that no longer exist
|
||||
@@ -20,11 +65,11 @@ export type IncompatibilityInfo = {
|
||||
}>; // Connected inputs where the type has changed
|
||||
};
|
||||
|
||||
export type SubAgentUpdateInfo = {
|
||||
export type SubAgentUpdateInfo<T extends GraphMetaLike = GraphMetaLike> = {
|
||||
hasUpdate: boolean;
|
||||
currentVersion: number;
|
||||
latestVersion: number;
|
||||
latestFlow: GraphMeta | null;
|
||||
latestFlow: T | null;
|
||||
isCompatible: boolean;
|
||||
incompatibilities: IncompatibilityInfo | null;
|
||||
};
|
||||
@@ -32,15 +77,15 @@ export type SubAgentUpdateInfo = {
|
||||
/**
|
||||
* Checks if a newer version of a sub-agent is available and determines compatibility
|
||||
*/
|
||||
export function useSubAgentUpdate(
|
||||
export function useSubAgentUpdate<T extends GraphMetaLike>(
|
||||
nodeId: string,
|
||||
graphID: string | undefined,
|
||||
graphVersion: number | undefined,
|
||||
currentInputSchema: GraphInputSchema | undefined,
|
||||
currentOutputSchema: GraphOutputSchema | undefined,
|
||||
connections: ConnectionData,
|
||||
availableFlows: GraphMeta[],
|
||||
): SubAgentUpdateInfo {
|
||||
connections: ConnectionData | EdgeLike[],
|
||||
availableFlows: T[],
|
||||
): SubAgentUpdateInfo<T> {
|
||||
// Find the latest version of the same graph
|
||||
const latestFlow = useMemo(() => {
|
||||
if (!graphID) return null;
|
||||
@@ -50,7 +95,7 @@ export function useSubAgentUpdate(
|
||||
// Check if there's an update available
|
||||
const hasUpdate = useMemo(() => {
|
||||
if (!latestFlow || graphVersion === undefined) return false;
|
||||
return latestFlow.version > graphVersion;
|
||||
return latestFlow.version! > graphVersion;
|
||||
}, [latestFlow, graphVersion]);
|
||||
|
||||
// Get connected input and output handles for this specific node
|
||||
@@ -81,15 +126,13 @@ export function useSubAgentUpdate(
|
||||
return { isCompatible: true, incompatibilities: null };
|
||||
}
|
||||
|
||||
const newInputSchema = latestFlow.input_schema;
|
||||
const newOutputSchema = latestFlow.output_schema;
|
||||
const newInputProps = newInputSchema?.properties || {};
|
||||
const newOutputProps = newOutputSchema?.properties || {};
|
||||
const newRequiredInputs = newInputSchema?.required || [];
|
||||
const newInputProps = getSchemaProperties(latestFlow.input_schema);
|
||||
const newOutputProps = getSchemaProperties(latestFlow.output_schema);
|
||||
const newRequiredInputs = getSchemaRequired(latestFlow.input_schema);
|
||||
|
||||
const currentInputProps = currentInputSchema?.properties || {};
|
||||
const currentOutputProps = currentOutputSchema?.properties || {};
|
||||
const currentRequiredInputs = currentInputSchema?.required || [];
|
||||
const currentInputProps = getSchemaProperties(currentInputSchema);
|
||||
const currentOutputProps = getSchemaProperties(currentOutputSchema);
|
||||
const currentRequiredInputs = getSchemaRequired(currentInputSchema);
|
||||
|
||||
const incompatibilities: IncompatibilityInfo = {
|
||||
missingInputs: [],
|
||||
@@ -187,7 +230,7 @@ export function useSubAgentUpdate(
|
||||
*/
|
||||
export function createUpdatedHardcodedValues(
|
||||
currentHardcodedValues: Record<string, unknown>,
|
||||
latestFlow: GraphMeta,
|
||||
latestFlow: GraphMetaLike,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...currentHardcodedValues,
|
||||
@@ -198,10 +241,11 @@ export function createUpdatedHardcodedValues(
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which edges are broken after an incompatible update
|
||||
* Determines which edges are broken after an incompatible update.
|
||||
* Works with both legacy ConnectionData (edge_id) and new CustomEdge (id).
|
||||
*/
|
||||
export function getBrokenEdgeIDs(
|
||||
connections: ConnectionData,
|
||||
connections: ConnectionData | EdgeLike[],
|
||||
incompatibilities: IncompatibilityInfo,
|
||||
nodeID: string,
|
||||
): string[] {
|
||||
@@ -211,13 +255,17 @@ export function getBrokenEdgeIDs(
|
||||
);
|
||||
|
||||
connections.forEach((conn) => {
|
||||
// Get edge ID - new builder uses `id`, legacy uses `edge_id`
|
||||
const edgeID = "id" in conn ? conn.id : conn.edge_id;
|
||||
if (!edgeID) return;
|
||||
|
||||
// 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.edge_id);
|
||||
brokenEdgeIds.push(edgeID);
|
||||
}
|
||||
|
||||
// Check if this connection uses an input with a type mismatch (node is target)
|
||||
@@ -226,7 +274,7 @@ export function getBrokenEdgeIDs(
|
||||
conn.targetHandle &&
|
||||
typeMismatchInputNames.has(conn.targetHandle)
|
||||
) {
|
||||
brokenEdgeIds.push(conn.edge_id);
|
||||
brokenEdgeIds.push(edgeID);
|
||||
}
|
||||
|
||||
// Check if this connection uses a missing output (node is source)
|
||||
@@ -235,7 +283,7 @@ export function getBrokenEdgeIDs(
|
||||
conn.sourceHandle &&
|
||||
incompatibilities.missingOutputs.includes(conn.sourceHandle)
|
||||
) {
|
||||
brokenEdgeIds.push(conn.edge_id);
|
||||
brokenEdgeIds.push(edgeID);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||
|
||||
interface GraphStore {
|
||||
graphExecutionStatus: AgentExecutionStatus | undefined;
|
||||
@@ -17,6 +18,10 @@ interface GraphStore {
|
||||
outputSchema: Record<string, any> | null,
|
||||
) => void;
|
||||
|
||||
// Available graphs; used for sub-graph updates
|
||||
availableSubGraphs: GraphMeta[];
|
||||
setAvailableSubGraphs: (graphs: GraphMeta[]) => void;
|
||||
|
||||
hasInputs: () => boolean;
|
||||
hasCredentials: () => boolean;
|
||||
hasOutputs: () => boolean;
|
||||
@@ -29,6 +34,7 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
||||
inputSchema: null,
|
||||
credentialsInputSchema: null,
|
||||
outputSchema: null,
|
||||
availableSubGraphs: [],
|
||||
|
||||
setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => {
|
||||
set({
|
||||
@@ -46,6 +52,8 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
||||
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
|
||||
set({ inputSchema, credentialsInputSchema, outputSchema }),
|
||||
|
||||
setAvailableSubGraphs: (graphs) => set({ availableSubGraphs: graphs }),
|
||||
|
||||
hasOutputs: () => {
|
||||
const { outputSchema } = get();
|
||||
return Object.keys(outputSchema?.properties ?? {}).length > 0;
|
||||
|
||||
@@ -13,6 +13,25 @@ import { useHistoryStore } from "./historyStore";
|
||||
import { useEdgeStore } from "./edgeStore";
|
||||
import { BlockUIType } from "../components/types";
|
||||
import { pruneEmptyValues } from "@/lib/utils";
|
||||
import { IncompatibilityInfo } from "../hooks/useSubAgentUpdate";
|
||||
|
||||
// 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
|
||||
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
||||
@@ -61,7 +80,23 @@ type NodeStore = {
|
||||
backendId: string,
|
||||
errors: { [key: string]: string },
|
||||
) => void;
|
||||
clearAllNodeErrors: () => void; // Add this
|
||||
clearAllNodeErrors: () => void;
|
||||
|
||||
// Sub-agent resolution mode state
|
||||
nodesInResolutionMode: Set<string>;
|
||||
brokenEdgeIDs: Set<string>;
|
||||
nodeResolutionData: Map<string, NodeResolutionData>; // nodeId -> resolution data
|
||||
setNodeResolutionMode: (
|
||||
nodeId: string,
|
||||
inResolution: boolean,
|
||||
resolutionData?: NodeResolutionData,
|
||||
) => void;
|
||||
isNodeInResolutionMode: (nodeId: string) => boolean;
|
||||
getNodeResolutionData: (nodeId: string) => NodeResolutionData | undefined;
|
||||
setBrokenEdgeIDs: (edgeIds: string[]) => void;
|
||||
removeBrokenEdgeID: (edgeId: string) => void;
|
||||
isEdgeBroken: (edgeId: string) => boolean;
|
||||
clearResolutionState: () => void;
|
||||
};
|
||||
|
||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
@@ -305,4 +340,67 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
// Sub-agent resolution mode state
|
||||
nodesInResolutionMode: new Set<string>(),
|
||||
brokenEdgeIDs: new 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);
|
||||
|
||||
if (inResolution) {
|
||||
newNodesSet.add(nodeID);
|
||||
if (resolutionData) {
|
||||
newResolutionDataMap.set(nodeID, resolutionData);
|
||||
}
|
||||
} else {
|
||||
newNodesSet.delete(nodeID);
|
||||
newResolutionDataMap.delete(nodeID);
|
||||
}
|
||||
|
||||
return {
|
||||
nodesInResolutionMode: newNodesSet,
|
||||
nodeResolutionData: newResolutionDataMap,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
isNodeInResolutionMode: (nodeId: string) => {
|
||||
return get().nodesInResolutionMode.has(nodeId);
|
||||
},
|
||||
|
||||
getNodeResolutionData: (nodeId: string) => {
|
||||
return get().nodeResolutionData.get(nodeId);
|
||||
},
|
||||
|
||||
setBrokenEdgeIDs: (edgeIds: string[]) => {
|
||||
set({ brokenEdgeIDs: new Set(edgeIds) });
|
||||
},
|
||||
|
||||
removeBrokenEdgeID: (edgeId: string) => {
|
||||
set((state) => {
|
||||
const newSet = new Set(state.brokenEdgeIDs);
|
||||
newSet.delete(edgeId);
|
||||
return { brokenEdgeIDs: newSet };
|
||||
});
|
||||
},
|
||||
|
||||
isEdgeBroken: (edgeId: string) => {
|
||||
return get().brokenEdgeIDs.has(edgeId);
|
||||
},
|
||||
|
||||
clearResolutionState: () => {
|
||||
set({
|
||||
nodesInResolutionMode: new Set<string>(),
|
||||
brokenEdgeIDs: new Set<string>(),
|
||||
nodeResolutionData: new Map<string, NodeResolutionData>(),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -18,7 +18,7 @@ function ErrorPageContent() {
|
||||
) {
|
||||
window.location.href = "/login";
|
||||
} else {
|
||||
window.location.href = "/marketplace";
|
||||
window.document.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ type FormContextType = {
|
||||
uiType?: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
brokenInputs?: Set<string>;
|
||||
typeMismatchInputs?: Map<string, string>; // Map of input name -> new type
|
||||
};
|
||||
|
||||
type FormRendererProps = {
|
||||
@@ -21,6 +23,7 @@ type FormRendererProps = {
|
||||
uiSchema: any;
|
||||
initialValues: any;
|
||||
formContext: FormContextType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const FormRenderer = ({
|
||||
@@ -29,25 +32,25 @@ export const FormRenderer = ({
|
||||
uiSchema,
|
||||
initialValues,
|
||||
formContext,
|
||||
className,
|
||||
}: FormRendererProps) => {
|
||||
const preprocessedSchema = useMemo(() => {
|
||||
return preprocessInputSchema(jsonSchema);
|
||||
}, [jsonSchema]);
|
||||
return (
|
||||
<div className={"mt-4"}>
|
||||
<Form
|
||||
schema={preprocessedSchema}
|
||||
validator={customValidator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={formContext}
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
noValidate={true}
|
||||
liveValidate={false}
|
||||
/>
|
||||
</div>
|
||||
<Form
|
||||
className={className}
|
||||
schema={preprocessedSchema}
|
||||
validator={customValidator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={formContext}
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
noValidate={true}
|
||||
liveValidate={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,11 +56,25 @@ export const AnyOfField = ({
|
||||
.join("_") || ""
|
||||
: generateHandleId(idSchema.$id ?? "");
|
||||
|
||||
const updatedFormContexrt = { ...formContext, fromAnyOf: true };
|
||||
const updatedFormContext = { ...formContext, fromAnyOf: true };
|
||||
|
||||
const { nodeId, showHandles = true } = updatedFormContexrt;
|
||||
const {
|
||||
nodeId,
|
||||
showHandles = true,
|
||||
brokenInputs,
|
||||
typeMismatchInputs,
|
||||
} = updatedFormContext;
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const isConnected = showHandles ? isInputConnected(nodeId, handleId) : false;
|
||||
// Check if this input is broken (from formContext, passed by FormCreator)
|
||||
const isBroken = handleId
|
||||
? ((brokenInputs as Set<string> | undefined)?.has(handleId) ?? false)
|
||||
: false;
|
||||
// Check if this input has a type mismatch, and get the new type if so
|
||||
const newTypeFromMismatch = handleId
|
||||
? (typeMismatchInputs as Map<string, string> | undefined)?.get(handleId)
|
||||
: undefined;
|
||||
const hasTypeMismatch = !!newTypeFromMismatch;
|
||||
const {
|
||||
isNullableType,
|
||||
nonNull,
|
||||
@@ -105,7 +119,7 @@ export const AnyOfField = ({
|
||||
name={name}
|
||||
registry={registry}
|
||||
disabled={disabled}
|
||||
formContext={updatedFormContexrt}
|
||||
formContext={updatedFormContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -125,7 +139,7 @@ export const AnyOfField = ({
|
||||
name={name}
|
||||
registry={registry}
|
||||
disabled={disabled}
|
||||
formContext={updatedFormContexrt}
|
||||
formContext={updatedFormContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -149,15 +163,24 @@ export const AnyOfField = ({
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
side="left"
|
||||
isBroken={isBroken}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
variant={formContext.size === "small" ? "body" : "body-medium"}
|
||||
className={cn(isBroken && "text-red-500 line-through")}
|
||||
>
|
||||
{schema.title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
</Text>
|
||||
<Text variant="small" className={colorClass}>
|
||||
({displayType} | null)
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn(
|
||||
colorClass,
|
||||
isBroken && "text-red-500 line-through",
|
||||
hasTypeMismatch && "rounded bg-red-100 px-1 !text-red-600",
|
||||
)}
|
||||
>
|
||||
({hasTypeMismatch ? newTypeFromMismatch : displayType} | null)
|
||||
</Text>
|
||||
</div>
|
||||
{!isConnected && (
|
||||
@@ -168,9 +191,9 @@ export const AnyOfField = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{!isConnected && isEnabled && renderInput(nonNull)}
|
||||
</div>
|
||||
{!isConnected && isEnabled && (
|
||||
<div className="mt-2">{renderInput(nonNull)}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -185,9 +208,13 @@ export const AnyOfField = ({
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
side="left"
|
||||
isBroken={isBroken}
|
||||
/>
|
||||
)}
|
||||
<Text variant={formContext.size === "small" ? "body" : "body-medium"}>
|
||||
<Text
|
||||
variant={formContext.size === "small" ? "body" : "body-medium"}
|
||||
className={cn(isBroken && "text-red-500 line-through")}
|
||||
>
|
||||
{schema.title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
</Text>
|
||||
{!isConnected && (
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ObjectField = (props: FieldProps) => {
|
||||
}
|
||||
|
||||
const fieldKey = generateHandleId(idSchema.$id ?? "");
|
||||
const { nodeId } = formContext;
|
||||
const { nodeId, brokenInputs, typeMismatchInputs } = formContext;
|
||||
|
||||
return (
|
||||
<ObjectEditor
|
||||
@@ -37,6 +37,8 @@ export const ObjectField = (props: FieldProps) => {
|
||||
value={formData}
|
||||
onChange={onChange}
|
||||
placeholder={`Enter ${name || "Contact Data"}`}
|
||||
brokenInputs={brokenInputs}
|
||||
typeMismatchInputs={typeMismatchInputs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,13 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
uiSchema,
|
||||
}) => {
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId, showHandles = true, size = "small" } = formContext;
|
||||
const {
|
||||
nodeId,
|
||||
showHandles = true,
|
||||
size = "small",
|
||||
brokenInputs,
|
||||
typeMismatchInputs,
|
||||
} = formContext;
|
||||
const uiType = formContext.uiType;
|
||||
|
||||
const showAdvanced = useNodeStore(
|
||||
@@ -56,7 +62,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
const isCredential = isCredentialFieldSchema(schema);
|
||||
const suppressHandle = isAnyOf || isOneOf;
|
||||
|
||||
let handleId = null;
|
||||
let handleId: string | null = null;
|
||||
if (!isArrayItem) {
|
||||
if (uiType === BlockUIType.AGENT) {
|
||||
const parts = fieldId.split("_");
|
||||
@@ -72,6 +78,13 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
}
|
||||
|
||||
const isConnected = showHandles ? isInputConnected(nodeId, handleId) : false;
|
||||
// Check if this input is broken (from formContext, passed by FormCreator)
|
||||
const isBroken = handleId ? (brokenInputs?.has(handleId) ?? false) : false;
|
||||
// Check if this input has a type mismatch, and get the new type if so
|
||||
const newTypeFromMismatch = handleId
|
||||
? (typeMismatchInputs as Map<string, string> | undefined)?.get(handleId)
|
||||
: undefined;
|
||||
const hasTypeMismatch = !!newTypeFromMismatch;
|
||||
|
||||
if (!showAdvanced && schema.advanced === true && !isConnected) {
|
||||
return null;
|
||||
@@ -113,7 +126,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 space-y-2",
|
||||
"mb-4 space-y-2 last:mb-0", // peer-to-peer spacing since we can't style the parent with gap-y
|
||||
fromAnyOf && "mb-0",
|
||||
size === "small" ? "w-[350px]" : "w-full",
|
||||
)}
|
||||
@@ -125,6 +138,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
side="left"
|
||||
isBroken={isBroken}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
@@ -139,14 +153,21 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
uiType === BlockUIType.INPUT && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK_MANUAL && "ml-3",
|
||||
isBroken && "text-red-500 line-through",
|
||||
)}
|
||||
>
|
||||
{isCredential && credentialProvider
|
||||
? toDisplayName(credentialProvider) + " credentials"
|
||||
: schema.title || label}
|
||||
</Text>
|
||||
<Text variant="small" className={colorClass}>
|
||||
({displayType})
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn(
|
||||
colorClass,
|
||||
hasTypeMismatch && "rounded bg-red-100 px-1 !text-red-600",
|
||||
)}
|
||||
>
|
||||
({hasTypeMismatch ? newTypeFromMismatch : displayType})
|
||||
</Text>
|
||||
{required && <span style={{ color: "red" }}>*</span>}
|
||||
{description?.props?.description && (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
HandleIdType,
|
||||
parseKeyValueHandleId,
|
||||
} from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ObjectEditorProps {
|
||||
id: string;
|
||||
@@ -22,6 +23,8 @@ export interface ObjectEditorProps {
|
||||
className?: string;
|
||||
nodeId: string;
|
||||
fieldKey: string;
|
||||
brokenInputs?: Set<string>;
|
||||
typeMismatchInputs?: Map<string, string>; // Map of input name -> new type
|
||||
}
|
||||
|
||||
export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
@@ -34,6 +37,8 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
disabled = false,
|
||||
className,
|
||||
nodeId,
|
||||
brokenInputs,
|
||||
typeMismatchInputs,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -96,7 +101,7 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex flex-col gap-2 ${className || ""}`}
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
id={parentFieldId}
|
||||
>
|
||||
{Object.entries(value).map(([key, propertyValue], idx) => {
|
||||
@@ -106,6 +111,13 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
HandleIdType.KEY_VALUE,
|
||||
);
|
||||
const isDynamicPropertyConnected = isInputConnected(nodeId, handleId);
|
||||
const isBroken = handleId
|
||||
? (brokenInputs?.has(handleId) ?? false)
|
||||
: false;
|
||||
const newTypeFromMismatch = handleId
|
||||
? typeMismatchInputs?.get(handleId)
|
||||
: undefined;
|
||||
const hasTypeMismatch = !!newTypeFromMismatch;
|
||||
|
||||
return (
|
||||
<div key={idx} className="flex flex-col gap-2">
|
||||
@@ -115,13 +127,27 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
isConnected={isDynamicPropertyConnected}
|
||||
handleId={handleId}
|
||||
side="left"
|
||||
isBroken={isBroken}
|
||||
/>
|
||||
)}
|
||||
<Text variant="small" className="!text-gray-500">
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn(
|
||||
"!text-gray-500",
|
||||
isBroken && "!text-red-500 line-through",
|
||||
)}
|
||||
>
|
||||
#{key.trim() === "" ? "" : key}
|
||||
</Text>
|
||||
<Text variant="small" className="!text-green-500">
|
||||
(string)
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn(
|
||||
"!text-green-500",
|
||||
isBroken && "!text-red-500 line-through",
|
||||
hasTypeMismatch && "rounded bg-red-100 px-1 !text-red-600",
|
||||
)}
|
||||
>
|
||||
({hasTypeMismatch ? newTypeFromMismatch : "string"})
|
||||
</Text>
|
||||
</div>
|
||||
{!isDynamicPropertyConnected && (
|
||||
|
||||
Reference in New Issue
Block a user