mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): add saving ability in new builder (#11148)
This PR introduces saving functionality to the new builder interface, allowing users to save and update agent flows. The implementation includes both UI components and backend integration for persistent storage of agent configurations. https://github.com/user-attachments/assets/95ee46de-2373-4484-9f34-5f09aa071c5e ### Key Features Added: #### 1. **Save Control Component** (`NewSaveControl`) - Added a new save control popover in the control panel with form inputs for agent name, description, and version display - Integrated with the new control panel as a primary action button with a floppy disk icon - Supports keyboard shortcuts (Ctrl+S / Cmd+S) for quick saving #### 2. **Graph Persistence Logic** - Implemented `useNewSaveControl` hook to handle: - Creating new graphs via `usePostV1CreateNewGraph` - Updating existing graphs via `usePutV1UpdateGraphVersion` - Intelligent comparison to prevent unnecessary saves when no changes are made - URL parameter management for flowID and flowVersion tracking #### 3. **Loading State Management** - Added `GraphLoadingBox` component to display a loading indicator while graphs are being fetched - Enhanced `useFlow` hook with loading state tracking (`isFlowContentLoading`) - Improved UX with clear visual feedback during graph operations #### 4. **Component Reorganization** - Refactored components from `NewBlockMenu` to `NewControlPanel` directory structure for better organization: - Moved all block menu related components under `NewControlPanel/NewBlockMenu/` - Separated save control into its own module (`NewControlPanel/NewSaveControl/`) - Improved modularity and separation of concerns #### 5. **State Management Enhancements** - Added `controlPanelStore` for managing control panel states (e.g., save popover visibility) - Enhanced `nodeStore` with `getBackendNodes()` method for retrieving nodes in backend format - Added `getBackendLinks()` to `edgeStore` for consistent link formatting ### Technical Improvements: - **Graph Comparison Logic**: Implemented `graphsEquivalent()` helper to deeply compare saved and current graph states, preventing redundant saves - **Form Validation**: Used Zod schema validation for save form inputs with proper constraints - **Error Handling**: Comprehensive error handling with user-friendly toast notifications - **Query Invalidation**: Proper cache invalidation after successful saves to ensure data consistency ### UI/UX Enhancements: - Clean, modern save dialog with clear labeling and placeholder text - Real-time version display showing the current graph version - Disabled state for save button during operations to prevent double submissions - Toast notifications for success and error states - Higher z-index for GraphLoadingBox to ensure visibility over other elements ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Saving is working perfectly. All nodes, links, their positions, and hardcoded data are saved correctly. - [x] If there are no changes, the user cannot save the graph.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ReactFlow, Background, Controls } from "@xyflow/react";
|
||||
import NewControlPanel from "../../NewBlockMenu/NewControlPanel/NewControlPanel";
|
||||
import NewControlPanel from "../../NewControlPanel/NewControlPanel";
|
||||
import CustomEdge from "../edges/CustomEdge";
|
||||
import { useFlow } from "./useFlow";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
@@ -7,6 +7,7 @@ import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useMemo } from "react";
|
||||
import { CustomNode } from "../nodes/CustomNode";
|
||||
import { useCustomEdge } from "../edges/useCustomEdge";
|
||||
import { GraphLoadingBox } from "./GraphLoadingBox";
|
||||
|
||||
export const Flow = () => {
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
@@ -17,7 +18,7 @@ export const Flow = () => {
|
||||
const { edges, onConnect, onEdgesChange } = useCustomEdge();
|
||||
|
||||
// We use this hook to load the graph and convert them into custom nodes and edges.
|
||||
useFlow();
|
||||
const { isFlowContentLoading } = useFlow();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full dark:bg-slate-900">
|
||||
@@ -36,6 +37,7 @@ export const Flow = () => {
|
||||
<Background />
|
||||
<Controls />
|
||||
<NewControlPanel />
|
||||
{isFlowContentLoading && <GraphLoadingBox />}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export const GraphLoadingBox = () => {
|
||||
return (
|
||||
<div className="absolute left-[50%] top-[50%] z-[99] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="flex flex-col items-center gap-4 rounded-xlarge border border-gray-200 bg-white p-8 shadow-lg dark:border-gray-700 dark:bg-slate-800">
|
||||
<div className="relative h-12 w-12">
|
||||
<div className="absolute inset-0 animate-spin rounded-full border-4 border-gray-200 border-t-black dark:border-gray-700 dark:border-t-blue-400"></div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Text variant="h4">Loading Flow</Text>
|
||||
<Text variant="small">Please wait while we load your graph...</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ export const useFlow = () => {
|
||||
flowVersion: parseAsInteger,
|
||||
});
|
||||
|
||||
const { data: graph } = useGetV1GetSpecificGraph(
|
||||
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
|
||||
flowID ?? "",
|
||||
flowVersion !== null ? { version: flowVersion } : {},
|
||||
{
|
||||
@@ -32,15 +32,16 @@ export const useFlow = () => {
|
||||
const nodes = graph?.nodes;
|
||||
const blockIds = nodes?.map((node) => node.block_id);
|
||||
|
||||
const { data: blocks } = useGetV2GetSpecificBlocks(
|
||||
{ block_ids: blockIds ?? [] },
|
||||
{
|
||||
query: {
|
||||
select: (res) => res.data as BlockInfo[],
|
||||
enabled: !!flowID && !!blockIds,
|
||||
const { data: blocks, isLoading: isBlocksLoading } =
|
||||
useGetV2GetSpecificBlocks(
|
||||
{ block_ids: blockIds ?? [] },
|
||||
{
|
||||
query: {
|
||||
select: (res) => res.data as BlockInfo[],
|
||||
enabled: !!flowID && !!blockIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const customNodes = useMemo(() => {
|
||||
if (!nodes || !blocks) return [];
|
||||
@@ -57,13 +58,22 @@ export const useFlow = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (customNodes.length > 0) {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
addNodes(customNodes);
|
||||
}
|
||||
|
||||
if (graph?.links) {
|
||||
useEdgeStore.getState().setConnections([]);
|
||||
addLinks(graph.links);
|
||||
}
|
||||
}, [customNodes, addNodes, graph?.links]);
|
||||
|
||||
return {};
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
useEdgeStore.getState().setConnections([]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isFlowContentLoading: isGraphLoading || isBlocksLoading };
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export type CustomNodeData = {
|
||||
[key: string]: any;
|
||||
};
|
||||
title: string;
|
||||
block_id: string;
|
||||
description: string;
|
||||
inputSchema: RJSFSchema;
|
||||
outputSchema: RJSFSchema;
|
||||
|
||||
@@ -24,6 +24,9 @@ export const FormCreator = React.memo(
|
||||
(state) => state.getHardCodedValues,
|
||||
);
|
||||
const handleChange = ({ formData }: any) => {
|
||||
if ("credentials" in formData && !formData.credentials?.id) {
|
||||
delete formData.credentials;
|
||||
}
|
||||
updateNodeData(nodeId, { hardcodedValues: formData });
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const OutputHandler = ({
|
||||
}) => {
|
||||
const { isOutputConnected } = useEdgeStore();
|
||||
const properties = outputSchema?.properties || {};
|
||||
const [isOutputVisible, setIsOutputVisible] = useState(false);
|
||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xl border-t border-slate-200/50 bg-white py-3.5">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "./helpers";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const useCredentialField = ({
|
||||
credentialSchema,
|
||||
@@ -34,8 +35,8 @@ export const useCredentialField = ({
|
||||
},
|
||||
});
|
||||
|
||||
const hardcodedValues = useNodeStore((state) =>
|
||||
state.getHardCodedValues(nodeId),
|
||||
const hardcodedValues = useNodeStore(
|
||||
useShallow((state) => state.getHardCodedValues(nodeId)),
|
||||
);
|
||||
|
||||
const credentialProvider = getCredentialProviderFromSchema(
|
||||
|
||||
@@ -40,7 +40,6 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[nodeId] ?? false,
|
||||
);
|
||||
const formData = useNodeStore((state) => state.getHardCodedValues(nodeId));
|
||||
|
||||
const { isArrayItem, arrayFieldHandleId } = useContext(ArrayEditorContext);
|
||||
|
||||
@@ -71,7 +70,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
let credentialProvider = null;
|
||||
if (isCredential) {
|
||||
credentialProvider = getCredentialProviderFromSchema(
|
||||
formData,
|
||||
useNodeStore.getState().getHardCodedValues(nodeId),
|
||||
schema as BlockIOCredentialsSubSchema,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface useBlockMenuProps {
|
||||
// pinBlocksPopover: boolean;
|
||||
setBlockMenuSelected: React.Dispatch<
|
||||
React.SetStateAction<"" | "save" | "block" | "search">
|
||||
>;
|
||||
}
|
||||
|
||||
export const useBlockMenu = ({
|
||||
// pinBlocksPopover,
|
||||
setBlockMenuSelected,
|
||||
}: useBlockMenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onOpen = (newOpen: boolean) => {
|
||||
// if (!pinBlocksPopover) {
|
||||
setOpen(newOpen);
|
||||
setBlockMenuSelected(newOpen ? "block" : "");
|
||||
// }
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
onOpen,
|
||||
};
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/__legacy__/ui/popover";
|
||||
import { Card, CardContent, CardFooter } from "@/components/__legacy__/ui/card";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { Button } from "@/components/__legacy__/ui/button";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { Label } from "@/components/__legacy__/ui/label";
|
||||
import { IconSave } from "@/components/__legacy__/ui/icons";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { ControlPanelButton } from "../ControlPanelButton";
|
||||
|
||||
interface SaveControlProps {
|
||||
agentMeta: GraphMeta | null;
|
||||
agentName: string;
|
||||
agentDescription: string;
|
||||
canSave: boolean;
|
||||
onSave: () => void;
|
||||
onNameChange: (name: string) => void;
|
||||
onDescriptionChange: (description: string) => void;
|
||||
pinSavePopover: boolean;
|
||||
|
||||
blockMenuSelected: "save" | "block" | "search" | "";
|
||||
setBlockMenuSelected: React.Dispatch<
|
||||
React.SetStateAction<"" | "save" | "block" | "search">
|
||||
>;
|
||||
}
|
||||
|
||||
export const NewSaveControl = ({
|
||||
agentMeta,
|
||||
canSave,
|
||||
onSave,
|
||||
agentName,
|
||||
onNameChange,
|
||||
agentDescription,
|
||||
onDescriptionChange,
|
||||
blockMenuSelected,
|
||||
setBlockMenuSelected,
|
||||
pinSavePopover,
|
||||
}: SaveControlProps) => {
|
||||
const handleSave = useCallback(() => {
|
||||
onSave();
|
||||
}, [onSave]);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
handleSave();
|
||||
toast({
|
||||
duration: 2000,
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleSave, toast]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={pinSavePopover ? true : undefined}
|
||||
onOpenChange={(open) => open || setBlockMenuSelected("")}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<ControlPanelButton
|
||||
data-id="save-control-popover-trigger"
|
||||
data-testid="blocks-control-save-button"
|
||||
selected={blockMenuSelected === "save"}
|
||||
onClick={() => {
|
||||
setBlockMenuSelected("save");
|
||||
}}
|
||||
className="rounded-none"
|
||||
>
|
||||
{/* Need to find phosphor icon alternative for this lucide icon */}
|
||||
<IconSave className="h-5 w-5" strokeWidth={2} />
|
||||
</ControlPanelButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
side="right"
|
||||
sideOffset={16}
|
||||
align="start"
|
||||
className="w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
|
||||
data-id="save-control-popover-content"
|
||||
>
|
||||
<Card className="border-none shadow-none dark:bg-slate-900">
|
||||
<CardContent className="p-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name" className="dark:text-gray-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Enter your agent name"
|
||||
className="col-span-3"
|
||||
value={agentName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
data-id="save-control-name-input"
|
||||
data-testid="save-control-name-input"
|
||||
maxLength={100}
|
||||
/>
|
||||
<Label htmlFor="description" className="dark:text-gray-300">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Your agent description"
|
||||
className="col-span-3"
|
||||
value={agentDescription}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
data-id="save-control-description-input"
|
||||
data-testid="save-control-description-input"
|
||||
maxLength={500}
|
||||
/>
|
||||
{agentMeta?.version && (
|
||||
<>
|
||||
<Label htmlFor="version" className="dark:text-gray-300">
|
||||
Version
|
||||
</Label>
|
||||
<Input
|
||||
id="version"
|
||||
placeholder="Version"
|
||||
className="col-span-3"
|
||||
value={agentMeta?.version || "-"}
|
||||
disabled
|
||||
data-testid="save-control-version-output"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||
<Button
|
||||
className="w-full dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-800"
|
||||
onClick={handleSave}
|
||||
data-id="save-control-save-agent"
|
||||
data-testid="save-control-save-agent-button"
|
||||
disabled={!canSave}
|
||||
>
|
||||
Save Agent
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { beautifyString } from "@/lib/utils";
|
||||
import { useAllBlockContent } from "./useAllBlockContent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
|
||||
export const AllBlocksContent = () => {
|
||||
const {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Block } from "../Block";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
|
||||
interface BlocksListProps {
|
||||
@@ -5,35 +5,20 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/__legacy__/ui/popover";
|
||||
import { BlockMenuContent } from "../BlockMenuContent/BlockMenuContent";
|
||||
import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { useBlockMenu } from "./useBlockMenu";
|
||||
import { ControlPanelButton } from "../../ControlPanelButton";
|
||||
import { LegoIcon } from "@phosphor-icons/react";
|
||||
import { useControlPanelStore } from "@/app/(platform)/build/stores/controlPanelStore";
|
||||
|
||||
interface BlockMenuProps {
|
||||
// pinBlocksPopover: boolean;
|
||||
blockMenuSelected: "save" | "block" | "search" | "";
|
||||
setBlockMenuSelected: React.Dispatch<
|
||||
React.SetStateAction<"" | "save" | "block" | "search">
|
||||
>;
|
||||
}
|
||||
|
||||
export const BlockMenu: React.FC<BlockMenuProps> = ({
|
||||
// pinBlocksPopover,
|
||||
blockMenuSelected,
|
||||
setBlockMenuSelected,
|
||||
}) => {
|
||||
const { open: _open, onOpen } = useBlockMenu({
|
||||
// pinBlocksPopover,
|
||||
setBlockMenuSelected,
|
||||
});
|
||||
export const BlockMenu = () => {
|
||||
const { blockMenuOpen, setBlockMenuOpen } = useControlPanelStore();
|
||||
return (
|
||||
// pinBlocksPopover ? true : open
|
||||
<Popover onOpenChange={onOpen}>
|
||||
<Popover onOpenChange={setBlockMenuOpen}>
|
||||
<PopoverTrigger className="hover:cursor-pointer">
|
||||
<ControlPanelButton
|
||||
data-id="blocks-control-popover-trigger"
|
||||
data-testid="blocks-control-blocks-button"
|
||||
selected={blockMenuSelected === "block"}
|
||||
selected={blockMenuOpen}
|
||||
className="rounded-none"
|
||||
>
|
||||
{/* Need to find phosphor icon alternative for this lucide icon */}
|
||||
@@ -4,7 +4,7 @@ import { BlockMenuSearchBar } from "../BlockMenuSearchBar/BlockMenuSearchBar";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { BlockMenuDefault } from "../BlockMenuDefault/BlockMenuDefault";
|
||||
import { BlockMenuSearch } from "../BlockMenuSearch/BlockMenuSearch";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
export const BlockMenuContent = () => {
|
||||
const { searchQuery } = useBlockMenuStore();
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsContent } from "../IntegrationsContent/IntegrationsContent"
|
||||
import { MarketplaceAgentsContent } from "../MarketplaceAgentsContent/MarketplaceAgentsContent";
|
||||
import { MyAgentsContent } from "../MyAgentsContent/MyAgentsContent";
|
||||
import { SuggestionContent } from "../SuggestionContent/SuggestionContent";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { DefaultStateType } from "../types";
|
||||
|
||||
export const BlockMenuDefaultContent = () => {
|
||||
@@ -7,11 +7,11 @@ import { MarketplaceAgentBlock } from "../MarketplaceAgentBlock";
|
||||
import { Block } from "../Block";
|
||||
import { UGCAgentBlock } from "../UGCAgentBlock";
|
||||
import { getSearchItemType } from "./helper";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NoSearchResult } from "../NoSearchResult";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
|
||||
export const BlockMenuSearch = () => {
|
||||
const {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { useGetV2BuilderSearchInfinite } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { SearchResponse } from "@/app/api/__generated__/models/searchResponse";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { debounce } from "lodash";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MenuItem } from "../MenuItem";
|
||||
import { useBlockMenuSidebar } from "./useBlockMenuSidebar";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { DefaultStateType } from "../types";
|
||||
|
||||
export const BlockMenuSidebar = () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useGetV2GetBuilderItemCounts } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { CountResponse } from "@/app/api/__generated__/models/countResponse";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
export const useBlockMenuSidebar = () => {
|
||||
const { defaultState, setDefaultState } = useBlockMenuStore();
|
||||
@@ -5,8 +5,8 @@ import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { useIntegrationBlocks } from "./useIntegrationBlocks";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
export const IntegrationBlocks = () => {
|
||||
const { integration, setIntegration } = useBlockMenuStore();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useGetV2GetBuilderBlocksInfinite } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { BlockResponse } from "@/app/api/__generated__/models/blockResponse";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
import { IntegrationBlocks } from "../IntegrationBlocks/IntegrationBlocks";
|
||||
import { PaginatedIntegrationList } from "../PaginatedIntegrationList/PaginatedIntegrationList";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
export const IntegrationsContent = () => {
|
||||
const { integration } = useBlockMenuStore();
|
||||
@@ -4,7 +4,7 @@ import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteS
|
||||
import { usePaginatedIntegrationList } from "./usePaginatedIntegrationList";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
|
||||
export const PaginatedIntegrationList = () => {
|
||||
const { setIntegration } = useBlockMenuStore();
|
||||
@@ -4,8 +4,8 @@ import { Block } from "../Block";
|
||||
import { useSuggestionContent } from "./useSuggestionContent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useBlockMenuStore } from "../../../stores/blockMenuStore";
|
||||
import { useNodeStore } from "../../../../stores/nodeStore";
|
||||
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
|
||||
import { DefaultStateType } from "../types";
|
||||
|
||||
export const SuggestionContent = () => {
|
||||
@@ -1,16 +1,18 @@
|
||||
// import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useMemo } from "react";
|
||||
import { BlockMenu } from "../BlockMenu/BlockMenu";
|
||||
import { BlockMenu } from "./NewBlockMenu/BlockMenu/BlockMenu";
|
||||
import { useNewControlPanel } from "./useNewControlPanel";
|
||||
// import { NewSaveControl } from "../SaveControl/NewSaveControl";
|
||||
import { GraphExecutionID } from "@/lib/autogpt-server-api";
|
||||
// import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { ArrowUUpLeftIcon, ArrowUUpRightIcon } from "@phosphor-icons/react";
|
||||
// import { GraphSearchMenu } from "../GraphMenu/GraphMenu";
|
||||
import { CustomNode } from "../../FlowEditor/nodes/CustomNode";
|
||||
import { CustomNode } from "../FlowEditor/nodes/CustomNode";
|
||||
import { history } from "@/app/(platform)/build/components/legacy-builder/history";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { NewSaveControl } from "./NewSaveControl/NewSaveControl";
|
||||
|
||||
export type Control = {
|
||||
icon: React.ReactNode;
|
||||
@@ -40,8 +42,6 @@ export const NewControlPanel = ({
|
||||
const _isGraphSearchEnabled = useGetFlag(Flag.GRAPH_SEARCH);
|
||||
|
||||
const {
|
||||
blockMenuSelected,
|
||||
setBlockMenuSelected,
|
||||
// agentDescription,
|
||||
// setAgentDescription,
|
||||
// saveAgent,
|
||||
@@ -74,15 +74,11 @@ export const NewControlPanel = ({
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"top- absolute left-4 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
|
||||
"absolute left-4 top-10 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
|
||||
<BlockMenu
|
||||
// pinBlocksPopover={pinBlocksPopover}
|
||||
blockMenuSelected={blockMenuSelected}
|
||||
setBlockMenuSelected={setBlockMenuSelected}
|
||||
/>
|
||||
<BlockMenu />
|
||||
{/* <Separator className="text-[#E1E1E1]" />
|
||||
{isGraphSearchEnabled && (
|
||||
<>
|
||||
@@ -107,20 +103,9 @@ export const NewControlPanel = ({
|
||||
>
|
||||
{control.icon}
|
||||
</ControlPanelButton>
|
||||
))}
|
||||
))} */}
|
||||
<Separator className="text-[#E1E1E1]" />
|
||||
<NewSaveControl
|
||||
agentMeta={savedAgent}
|
||||
canSave={!isSaving && !isRunning && !isStopping}
|
||||
onSave={saveAgent}
|
||||
agentDescription={agentDescription}
|
||||
onDescriptionChange={setAgentDescription}
|
||||
agentName={agentName}
|
||||
onNameChange={setAgentName}
|
||||
pinSavePopover={pinSavePopover}
|
||||
blockMenuSelected={blockMenuSelected}
|
||||
setBlockMenuSelected={setBlockMenuSelected}
|
||||
/> */}
|
||||
<NewSaveControl />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/__legacy__/ui/popover";
|
||||
import { Card, CardContent, CardFooter } from "@/components/__legacy__/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { useNewSaveControl } from "./useNewSaveControl";
|
||||
import { Form, FormField } from "@/components/__legacy__/ui/form";
|
||||
import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { FloppyDiskIcon } from "@phosphor-icons/react";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
export const NewSaveControl = () => {
|
||||
const { form, onSubmit, isLoading, graphVersion } = useNewSaveControl();
|
||||
const { saveControlOpen, setSaveControlOpen } = useControlPanelStore();
|
||||
return (
|
||||
<Popover onOpenChange={setSaveControlOpen}>
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<ControlPanelButton
|
||||
data-id="save-control-popover-trigger"
|
||||
data-testid="save-control-save-button"
|
||||
selected={saveControlOpen}
|
||||
className="rounded-none"
|
||||
>
|
||||
{/* Need to find phosphor icon alternative for this lucide icon */}
|
||||
<FloppyDiskIcon className="h-6 w-6" />
|
||||
</ControlPanelButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Save</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
sideOffset={15}
|
||||
align="start"
|
||||
data-id="save-control-popover-content"
|
||||
className="w-96 max-w-[400px] rounded-xlarge"
|
||||
>
|
||||
<Card className="border-none dark:bg-slate-900">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<CardContent className="p-0">
|
||||
<div className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
size="small"
|
||||
placeholder="Enter your agent name"
|
||||
data-id="save-control-name-input"
|
||||
data-testid="save-control-name-input"
|
||||
maxLength={100}
|
||||
wrapperClassName="!mb-0"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="description"
|
||||
size="small"
|
||||
label="Description"
|
||||
placeholder="Your agent description"
|
||||
data-id="save-control-description-input"
|
||||
data-testid="save-control-description-input"
|
||||
maxLength={500}
|
||||
wrapperClassName="!mb-0"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{graphVersion && (
|
||||
<Input
|
||||
id="version"
|
||||
placeholder="Version"
|
||||
size="small"
|
||||
value={graphVersion || "-"}
|
||||
disabled
|
||||
data-testid="save-control-version-output"
|
||||
label="Version"
|
||||
wrapperClassName="!mb-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
{/* TODO: Add a cron schedule button */}
|
||||
<CardFooter className="mt-3 flex flex-col items-stretch gap-2 p-0">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="small"
|
||||
className="w-full dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-800"
|
||||
data-id="save-control-save-agent"
|
||||
data-testid="save-control-save-agent-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Save Agent
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Graph } from "@/app/api/__generated__/models/graph";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { Link } from "@/app/api/__generated__/models/link";
|
||||
import { NodeModel } from "@/app/api/__generated__/models/nodeModel";
|
||||
import { Node } from "@/app/api/__generated__/models/node";
|
||||
import { deepEquals } from "@rjsf/utils";
|
||||
|
||||
export const graphsEquivalent = (
|
||||
saved: GraphModel | undefined,
|
||||
current: Graph | undefined,
|
||||
): boolean => {
|
||||
if (!saved || !current) {
|
||||
return false;
|
||||
}
|
||||
const sortNodes = (nodes: NodeModel[] | Node[]) =>
|
||||
nodes.toSorted((a, b) => a.id?.localeCompare(b.id ?? "") ?? 0);
|
||||
|
||||
const sortLinks = (links: Link[]) =>
|
||||
links.toSorted(
|
||||
(a, b) =>
|
||||
8 * a.source_id.localeCompare(b.source_id) +
|
||||
4 * a.sink_id.localeCompare(b.sink_id) +
|
||||
2 * a.source_name.localeCompare(b.source_name) +
|
||||
a.sink_name.localeCompare(b.sink_name),
|
||||
);
|
||||
const _saved = {
|
||||
name: saved.name,
|
||||
description: saved.description,
|
||||
nodes: sortNodes(saved.nodes ?? []).map((v) => ({
|
||||
block_id: v.block_id,
|
||||
input_default: v.input_default,
|
||||
metadata: v.metadata,
|
||||
})),
|
||||
links: sortLinks(saved.links ?? []).map((v) => ({
|
||||
sink_name: v.sink_name,
|
||||
source_name: v.source_name,
|
||||
})),
|
||||
};
|
||||
|
||||
// Normalize current graph - exclude IDs
|
||||
const _current = {
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
nodes: sortNodes(current.nodes ?? []).map(({ id: _, ...rest }) => rest),
|
||||
links: sortLinks(current.links ?? []).map(
|
||||
({ source_id: _, sink_id: __, ...rest }) => rest,
|
||||
),
|
||||
};
|
||||
|
||||
return deepEquals(_saved, _current);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import {
|
||||
getGetV1GetSpecificGraphQueryKey,
|
||||
useGetV1GetSpecificGraph,
|
||||
usePostV1CreateNewGraph,
|
||||
usePutV1UpdateGraphVersion,
|
||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { Graph } from "@/app/api/__generated__/models/graph";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { graphsEquivalent } from "./helpers";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required").max(100),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
type SaveableGraphFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export const useNewSaveControl = () => {
|
||||
const { setSaveControlOpen } = useControlPanelStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
});
|
||||
|
||||
const { data: graph } = useGetV1GetSpecificGraph(
|
||||
flowID ?? "",
|
||||
flowVersion !== null ? { version: flowVersion } : {},
|
||||
{
|
||||
query: {
|
||||
select: (res) => res.data as GraphModel,
|
||||
enabled: !!flowID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: createNewGraph, isPending: isCreating } =
|
||||
usePostV1CreateNewGraph({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const data = response.data as GraphModel;
|
||||
form.reset({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
});
|
||||
setSaveControlOpen(false);
|
||||
setQueryStates({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
toast({
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateGraph, isPending: isUpdating } =
|
||||
usePutV1UpdateGraphVersion({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const data = response.data as GraphModel;
|
||||
form.reset({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
});
|
||||
setSaveControlOpen(false);
|
||||
setQueryStates({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
toast({
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1GetSpecificGraphQueryKey(data.id),
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<SaveableGraphFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: graph?.name ?? "",
|
||||
description: graph?.description ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
// Handle Ctrl+S / Cmd+S keyboard shortcut
|
||||
useEffect(() => {
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
await onSubmit(form.getValues());
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graph) {
|
||||
form.reset({
|
||||
name: graph.name ?? "",
|
||||
description: graph.description ?? "",
|
||||
});
|
||||
}
|
||||
}, [graph, form]);
|
||||
|
||||
const onSubmit = async (values: SaveableGraphFormValues) => {
|
||||
const graphNodes = useNodeStore.getState().getBackendNodes();
|
||||
const graphLinks = useEdgeStore.getState().getBackendLinks();
|
||||
|
||||
if (graph && graph.id) {
|
||||
const data: Graph = {
|
||||
id: graph.id,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
nodes: graphNodes,
|
||||
links: graphLinks,
|
||||
};
|
||||
if (graphsEquivalent(graph, data)) {
|
||||
toast({
|
||||
title: "No changes to save",
|
||||
description: "The graph is the same as the saved version.",
|
||||
variant: "default",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await updateGraph({ graphId: graph.id, data: data });
|
||||
} else {
|
||||
const data: Graph = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
nodes: graphNodes,
|
||||
links: graphLinks,
|
||||
};
|
||||
await createNewGraph({ data: { graph: data } });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading: isCreating || isUpdating,
|
||||
graphVersion: graph?.version,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/components/__legacy__/ui/popover";
|
||||
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
|
||||
import { GraphSearchContent } from "../GraphMenuContent/GraphContent";
|
||||
import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { ControlPanelButton } from "../../ControlPanelButton";
|
||||
import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
|
||||
import { useGraphMenu } from "./useGraphMenu";
|
||||
|
||||
@@ -15,6 +15,7 @@ export const convertBlockInfoIntoCustomNodeData = (
|
||||
inputSchema: block.inputSchema,
|
||||
outputSchema: block.outputSchema,
|
||||
uiType: block.uiType as BlockUIType,
|
||||
block_id: block.id,
|
||||
};
|
||||
return customNodeData;
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ import RunnerUIWrapper, { RunnerUIWrapperRef } from "../RunnerUIWrapper";
|
||||
import OttoChatWidget from "@/app/(platform)/build/components/legacy-builder/OttoChatWidget";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useCopyPaste } from "../../../../../../hooks/useCopyPaste";
|
||||
import NewControlPanel from "@/app/(platform)/build/components/NewBlockMenu/NewControlPanel/NewControlPanel";
|
||||
import NewControlPanel from "@/app/(platform)/build/components/NewControlPanel/NewControlPanel";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { BuildActionBar } from "../BuildActionBar";
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { CustomNode } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
|
||||
import { GraphSearchContent } from "../NewBlockMenu/GraphMenuContent/GraphContent";
|
||||
import { GraphSearchContent } from "../NewControlPanel/NewSearchGraph/GraphMenuContent/GraphContent";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { useGraphMenu } from "../NewBlockMenu/GraphMenu/useGraphMenu";
|
||||
import { useGraphMenu } from "../NewControlPanel/NewSearchGraph/GraphMenu/useGraphMenu";
|
||||
|
||||
interface GraphSearchControlProps {
|
||||
nodes: CustomNode[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { DefaultStateType } from "../components/NewBlockMenu/types";
|
||||
import { DefaultStateType } from "../components/NewControlPanel/NewBlockMenu/types";
|
||||
|
||||
type BlockMenuStore = {
|
||||
searchQuery: string;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
type ControlPanelStore = {
|
||||
blockMenuOpen: boolean;
|
||||
saveControlOpen: boolean;
|
||||
setBlockMenuOpen: (open: boolean) => void;
|
||||
setSaveControlOpen: (open: boolean) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const useControlPanelStore = create<ControlPanelStore>((set) => ({
|
||||
blockMenuOpen: false,
|
||||
saveControlOpen: false,
|
||||
|
||||
setBlockMenuOpen: (open) => set({ blockMenuOpen: open }),
|
||||
setSaveControlOpen: (open) => set({ saveControlOpen: open }),
|
||||
reset: () =>
|
||||
set({
|
||||
blockMenuOpen: false,
|
||||
saveControlOpen: false,
|
||||
}),
|
||||
}));
|
||||
@@ -23,6 +23,7 @@ type EdgeStore = {
|
||||
getNodeConnections: (nodeId: string) => Connection[];
|
||||
isInputConnected: (nodeId: string, handle: string) => boolean;
|
||||
isOutputConnected: (nodeId: string, handle: string) => boolean;
|
||||
getBackendLinks: () => Link[];
|
||||
addLinks: (links: Link[]) => void;
|
||||
|
||||
getAllHandleIdsOfANode: (nodeId: string) => string[];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NodeChange, applyNodeChanges } from "@xyflow/react";
|
||||
import { CustomNode } from "../components/FlowEditor/nodes/CustomNode";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { convertBlockInfoIntoCustomNodeData } from "../components/helper";
|
||||
import { Node } from "@/app/api/__generated__/models/node";
|
||||
|
||||
type NodeStore = {
|
||||
nodes: CustomNode[];
|
||||
@@ -19,6 +20,8 @@ type NodeStore = {
|
||||
getShowAdvanced: (nodeId: string) => boolean;
|
||||
addNodes: (nodes: CustomNode[]) => void;
|
||||
getHardCodedValues: (nodeId: string) => Record<string, any>;
|
||||
convertCustomNodeToBackendNode: (node: CustomNode) => Node;
|
||||
getBackendNodes: () => Node[];
|
||||
};
|
||||
|
||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
@@ -84,4 +87,20 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
get().nodes.find((n) => n.id === nodeId)?.data?.hardcodedValues || {}
|
||||
);
|
||||
},
|
||||
convertCustomNodeToBackendNode: (node: CustomNode) => {
|
||||
return {
|
||||
id: node.id,
|
||||
block_id: node.data.block_id,
|
||||
input_default: node.data.hardcodedValues,
|
||||
metadata: {
|
||||
// TODO: Add more metadata
|
||||
position: node.position,
|
||||
},
|
||||
};
|
||||
},
|
||||
getBackendNodes: () => {
|
||||
return get().nodes.map((node) =>
|
||||
get().convertCustomNodeToBackendNode(node),
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -20,7 +20,7 @@ test("user can signup successfully", async ({ page }) => {
|
||||
|
||||
const marketplaceText = getText(
|
||||
"Bringing you AI agents designed by thinkers from around the world",
|
||||
);
|
||||
).first();
|
||||
|
||||
// Verify we're on marketplace and authenticated
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
Reference in New Issue
Block a user