Compare commits

...

99 Commits

Author SHA1 Message Date
Abhimanyu Yadav
7926919e3e Implement new block menu and improve UI components
The commit refactors the block menu and UI components with improved styling and functionality, including:

- Add new block menu system with search, filters and categories
- Implement dedicated components for different block types
- Create context provider for block menu state management
- Add support for marketplace agents and integrations
- Update control panel styling and layout
- Improve accessibility and user interactions
2025-06-09 16:08:54 +05:30
Abhimanyu Yadav
27e53aa3dd Comment out monitor test suite 2025-06-09 10:45:25 +05:30
Abhimanyu Yadav
a24673d15f Comment out build.spec.ts test file 2025-06-09 10:34:58 +05:30
Abhimanyu Yadav
9d2d9606e8 Merge branch 'dev' into redesigning-block-menu 2025-06-09 10:17:38 +05:30
Abhimanyu Yadav
91407dfc33 Add expandable creator list in filter sheet menu 2025-06-06 18:34:00 +05:30
abhi1992002
851919d2d5 Merge branch 'dev' into redesigning-block-menu 2025-06-06 18:23:17 +05:30
Abhimanyu Yadav
d6acb02cb6 Merge branch 'dev' into redesigning-block-menu 2025-06-06 18:20:05 +05:30
Krzysztof Czerwinski
9c07206725 Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-06-06 14:43:17 +02:00
Krzysztof Czerwinski
4bd3447301 Cleanup and comments 2025-06-06 14:39:13 +02:00
Abhimanyu Yadav
8adc9f967d Remove commented TODO and clean up code formatting 2025-06-06 17:11:40 +05:30
Abhimanyu Yadav
349b70c4bc Remove unused imports and cleanup effects 2025-06-06 17:07:59 +05:30
Abhimanyu Yadav
9ecfa1e1f1 Add scrolling and fixed footer to filter sheet panel 2025-06-06 17:03:24 +05:30
Krzysztof Czerwinski
4e17f9c49e Include block costs in get_blocks 2025-06-06 13:05:44 +02:00
Krzysztof Czerwinski
31fdeeb706 Make agent_name optional 2025-06-06 13:03:32 +02:00
Abhimanyu Yadav
e42b24c029 Add hasLocalActiveFilters for applying filter state 2025-06-06 11:10:47 +05:30
Abhimanyu Yadav
2d52a57a21 Fix "Agent page" link propagation in menu block 2025-06-06 10:53:55 +05:30
Krzysztof Czerwinski
f45123f6b6 Fix Agent Executor block name 2025-06-05 17:00:40 +02:00
Abhimanyu Yadav
d524518f41 Update pnpm-lock.yaml 2025-06-05 18:32:07 +05:30
abhi1992002
81d1b28d92 Merge remote-tracking branch 'upstream/dev' into redesigning-block-menu 2025-06-05 18:30:19 +05:30
Abhimanyu Yadav
4e4e754ac1 Merge branch 'backend-temp' into redesigning-block-menu 2025-06-05 17:00:08 +05:30
Krzysztof Czerwinski
e409d7aa34 Fixes 2025-06-04 14:58:49 +02:00
Abhimanyu Yadav
8312a339c2 cleaning up frontend code 2025-06-04 11:22:46 +05:30
Abhimanyu Yadav
5b45d246ef fix blockType utils 2025-06-04 10:46:45 +05:30
Krzysztof Czerwinski
5c7c7ca874 Suggested blocks 2025-06-03 19:27:02 +02:00
Krzysztof Czerwinski
c93c5e35ba Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-06-03 11:45:34 +02:00
Abhimanyu Yadav
ce989b1bf7 remove providers from filter list and add support of ai blocks in search
list]
2025-06-03 10:46:25 +05:30
Abhimanyu Yadav
c1c919b88b Merge branch 'backend-temp' into redesigning-block-menu 2025-06-03 10:22:01 +05:30
Krzysztof Czerwinski
21a91fe9fd Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-06-02 15:07:16 +02:00
Krzysztof Czerwinski
b2f3d8c1f2 Search model names 2025-06-02 15:06:54 +02:00
Krzysztof Czerwinski
46ab2e3b20 Remove providers filter from search 2025-06-02 10:13:05 +02:00
Abhimanyu Yadav
5b40700299 fetching creator list from searchList
Moves the `getBlockType` function from the SearchList component to the
`utils.ts` file to make it more reusable. Also removes the unused
`creators` state and `setCreators` function from the
BlockMenuContext and instead calculates the creators list dynamically
within the FilterSheet component based on the available search data.
2025-06-02 13:07:35 +05:30
Abhimanyu Yadav
1a97020eeb fix marketplace agent block and libray agent block in searchList 2025-06-02 12:50:07 +05:30
Abhimanyu Yadav
39d03f2090 Add Marketplace Agents to builder
Adds functionality to add Marketplace agents to the user's library and then to builder.
Includes a loading indicator while the agent is being added.
Refactors agent-to-block conversion into a utility function.
2025-06-02 12:32:13 +05:30
Abhimanyu Yadav
8088d294f4 Add Agent Blocks to Flow
This commit adds the ability to add Agent blocks to the
flow.  Clicking on an agent in the My Agents menu will add
it to the flow.  The block includes the necessary
information such as input/output schemas.
2025-06-02 12:03:51 +05:30
Abhimanyu Yadav
31266949ed Clears all filters when the search input is cleared and redesign filter based on new design. 2025-06-02 11:34:01 +05:30
abhi1992002
f4eb00a6ad Fetch Block Counts in Block Menu
Adds API calls to fetch block counts for each category
in the block menu and displays them next to the category
name.  This replaces the hardcoded numbers previously
displayed.
2025-06-02 10:50:26 +05:30
Abhimanyu Yadav
f75cc0dd11 Merge branch 'dev' into redesigning-block-menu 2025-06-02 10:34:16 +05:30
Krzysztof Czerwinski
21b612625f Format frontend 2025-05-31 13:42:32 +02:00
Krzysztof Czerwinski
eec0d276d5 Add output_schema to LibraryAgent 2025-05-31 13:42:07 +02:00
Krzysztof Czerwinski
c6941e7f6e Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-05-31 12:49:36 +02:00
Abhimanyu Yadav
325684a10f remove recent searches from suggestionContent and done some cleanup as
well
2025-05-30 17:33:44 +05:30
Abhimanyu Yadav
cf057cbbda fixed pagination problem in default menus in block menu 2025-05-30 17:26:59 +05:30
Abhimanyu Yadav
f3a7be1fd3 add highlighted description while searching 2025-05-30 12:06:36 +05:30
Abhimanyu Yadav
97bcb0f95e fix searchlist pagination 2025-05-30 11:11:40 +05:30
Abhimanyu Yadav
dd71d65706 adding beautify String in integration chips 2025-05-30 10:57:08 +05:30
Abhimanyu Yadav
2b2d26bcde remove items expanding when selecting menus 2025-05-30 10:54:56 +05:30
Abhimanyu Yadav
67f6f43e1b fix error state layout in input/output/action blocks list 2025-05-30 10:43:03 +05:30
Abhimanyu Yadav
a3409c9578 fix MarketplaceAgentBlock layout 2025-05-30 10:29:53 +05:30
Abhimanyu Yadav
7f82457ea4 add external agent link to marketplace agent block 2025-05-30 10:25:40 +05:30
Abhimanyu Yadav
a5c0fabc00 fix design of clear button in searchMenuBar 2025-05-30 10:12:09 +05:30
Krzysztof Czerwinski
09dba93a4a Add counts endpoint 2025-05-29 16:17:33 +02:00
Krzysztof Czerwinski
ea2cd3e7bf Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-05-29 13:16:18 +02:00
Abhimanyu Yadav
d3d0ccf732 fix menu item hover state and add a clear button at the end of searchbar 2025-05-29 11:09:55 +05:30
Abhimanyu Yadav
d8d5d6ec0c make hover state correct on all reusable compoents in block menu 2025-05-29 11:01:09 +05:30
Abhimanyu Yadav
f45b09c0b5 fix hover state and heading text in suggestion content page 2025-05-29 10:57:08 +05:30
Abhimanyu Yadav
1e89b6d3a4 add beautifyString in block, integration and integration block 2025-05-29 10:31:10 +05:30
Abhimanyu Yadav
950a85e179 fix image sizes warning with fill 2025-05-28 17:40:35 +05:30
Abhimanyu Yadav
c5e3148145 add better error handling in all components 2025-05-28 17:27:07 +05:30
Abhimanyu Yadav
a135ba3f0b refactor addBlock implementation in flow.tsx 2025-05-28 15:58:31 +05:30
Abhimanyu Yadav
fe95e27226 only show scroller when hovering 2025-05-28 15:48:28 +05:30
Abhimanyu Yadav
711ca10cc9 add relative time in my_agent block using react-timeago library 2025-05-28 15:29:34 +05:30
Abhimanyu Yadav
1346d8230c add 500ms debouncer on searchbar 2025-05-28 15:19:56 +05:30
Abhimanyu Yadav
07c84a4757 add categories filter in search 2025-05-28 13:57:39 +05:30
Abhimanyu Yadav
596824c1e7 add pagination on search list 2025-05-28 13:28:32 +05:30
Abhimanyu Yadav
79afa6db99 add search functioanlity in block menu 2025-05-28 12:15:38 +05:30
Abhimanyu Yadav
e034c16f31 add pagination in all components in default state 2025-05-26 21:13:51 +05:30
Abhimanyu Yadav
9012eff1ac add basic data fetching in all default state components 2025-05-26 10:27:15 +05:30
Abhimanyu Yadav
0361ea4aa4 connection integration list and blocks 2025-05-26 00:25:30 +05:30
Abhimanyu Yadav
6f1c522ea3 add some images and connect suggestion content frontend with backend 2025-05-25 23:09:22 +05:30
Krzysztof Czerwinski
2d654bf64b Update frontend types and api client 2025-05-25 15:12:01 +02:00
Krzysztof Czerwinski
bb69e32fee Update backend 2025-05-25 15:11:29 +02:00
Krzysztof Czerwinski
1be830835b Update signatures, disable providers 2025-05-23 17:22:35 +02:00
Krzysztof Czerwinski
a2a4d546f7 Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-05-23 16:51:53 +02:00
Krzysztof Czerwinski
3053a7bd06 Add types and function on the frontend 2025-05-23 16:50:52 +02:00
Krzysztof Czerwinski
bbf4108136 Add builder router and get_blocks endpoint 2025-05-23 16:50:05 +02:00
Krzysztof Czerwinski
95387bcf78 Add model and functions 2025-05-23 16:48:34 +02:00
Abhimanyu Yadav
e1fc56e6f3 fix small optimisation and DX issue 2025-05-21 18:10:29 +05:30
Abhimanyu Yadav
2a06956802 fix max width in sidebar 2025-05-20 17:01:03 +05:30
Abhimanyu Yadav
32231ff80f Implement search text highlighting in Block components, add transitions
to FilterChip, and create NoSearchResult component for empty searches. Move
SearchItem types to provider context for better access.
2025-05-20 15:31:02 +05:30
Abhimanyu Yadav
d0b23c085f add context api for block menu 2025-05-20 11:58:45 +05:30
Abhimanyu Yadav
e718d3d3d8 fix filter sheets 2025-05-20 11:25:46 +05:30
Abhimanyu Yadav
1971a62684 fix checkbox tick design 2025-05-20 10:38:51 +05:30
Abhimanyu Yadav
e125b5923c fix width of left sidebar 2025-05-20 10:30:18 +05:30
Abhimanyu Yadav
c6942e4e6f prevent layout shift when clicking result elements with border 2025-05-20 10:19:02 +05:30
Abhimanyu Yadav
c9e421a219 Merge branch 'dev' into redesigning-block-menu 2025-05-19 22:27:23 +05:30
Abhimanyu Yadav
7868373897 fix comments 2025-05-19 17:06:17 +05:30
Abhimanyu Yadav
f1c8399e0e fix recent searches onClick 2025-05-19 16:55:59 +05:30
Abhimanyu Yadav
97ba69ef1c fix lint 2025-05-19 16:35:55 +05:30
Abhimanyu Yadav
773e1488bf add filter sheet 2025-05-19 16:34:26 +05:30
Abhimanyu Yadav
4273be59ba fix format 2025-05-19 15:37:13 +05:30
Abhimanyu Yadav
06e524788a fix format 2025-05-18 21:10:26 +05:30
Abhimanyu Yadav
bc08012771 add search list in block menu 2025-05-18 21:10:19 +05:30
Abhimanyu Yadav
4af0aedebd fix format 2025-05-18 17:16:45 +05:30
Abhimanyu Yadav
d22464a75e Add skeleton components and loading states 2025-05-18 17:16:08 +05:30
Abhimanyu Yadav
82e3a485f0 complete frontend design for default state 2025-05-18 10:19:25 +05:30
Abhimanyu Yadav
8165ad5879 fix scrollbar in default content 2025-05-18 08:52:20 +05:30
Abhimanyu Yadav
451284de76 Add tailwind-scrollbar-hide and implement block menu UI
The commit adds a new block menu UI component with sidebar navigation,
integration chips, and scrollable content areas. It includes tailwind-
scrollbar-hide for better UI experience and custom CSS for scroll
containers. The implementation features different content sections
for blocks categorized by type (input, action, output) and supports
search functionality.
2025-05-17 21:18:08 +05:30
Abhimanyu Yadav
1d8c7c5e1a Merge branch 'dev' into redesigning-block-menu 2025-05-17 00:13:52 +05:30
Abhimanyu Yadav
34be6a3379 creating small ui reusable component 2025-05-17 00:01:40 +05:30
25 changed files with 1993 additions and 91 deletions

View File

@@ -28,6 +28,7 @@ import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import {
Block,
BlockUIType,
formatEdgeID,
GraphExecutionID,
@@ -53,6 +54,7 @@ import OttoChatWidget from "@/components/OttoChatWidget";
import { useToast } from "@/components/ui/use-toast";
import { useCopyPaste } from "../hooks/useCopyPaste";
import { CronScheduler } from "./cronScheduler";
import { BlockMenu } from "./builder/block-menu/BlockMenu";
// This is for the history, this is the minimum distance a block must move before it is logged
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
@@ -103,7 +105,6 @@ const FlowEditor: React.FC<{
setAgentDescription,
savedAgent,
availableNodes,
availableFlows,
getOutputType,
requestSave,
requestSaveAndRun,
@@ -138,6 +139,10 @@ const FlowEditor: React.FC<{
// State to control if save popover should be pinned open
const [pinSavePopover, setPinSavePopover] = useState(false);
const [blockMenuSelected, setBlockMenuSelected] = useState<
"save" | "block" | ""
>("");
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
const [openCron, setOpenCron] = useState(false);
@@ -471,13 +476,7 @@ const FlowEditor: React.FC<{
}, [nodes, setViewport, x, y]);
const addNode = useCallback(
(blockId: string, nodeType: string, hardcodedValues: any = {}) => {
const nodeSchema = availableNodes.find((node) => node.id === blockId);
if (!nodeSchema) {
console.error(`Schema not found for block ID: ${blockId}`);
return;
}
(block: Block) => {
/*
Calculate a position to the right of the newly added block, allowing for some margin.
If adding to the right side causes the new block to collide with an existing block, attempt to place it at the bottom or left.
@@ -494,7 +493,7 @@ const FlowEditor: React.FC<{
? // we will get all the dimension of nodes, then store
findNewlyAddedBlockCoordinates(
nodeDimensions,
nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500,
block.uiType == BlockUIType.NOTE ? 300 : 500,
60,
1.0,
)
@@ -509,19 +508,19 @@ const FlowEditor: React.FC<{
type: "custom",
position: viewportCoordinates, // Set the position to the calculated viewport center
data: {
blockType: nodeType,
blockCosts: nodeSchema.costs,
title: `${nodeType} ${nodeId}`,
description: nodeSchema.description,
categories: nodeSchema.categories,
inputSchema: nodeSchema.inputSchema,
outputSchema: nodeSchema.outputSchema,
hardcodedValues: hardcodedValues,
blockType: block.name,
blockCosts: block.costs,
title: `${block.name} ${nodeId}`,
description: block.description,
categories: block.categories,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: block.hardcodedValues || {},
connections: [],
isOutputOpen: false,
block_id: blockId,
isOutputStatic: nodeSchema.staticOutput,
uiType: nodeSchema.uiType,
block_id: block.id,
isOutputStatic: block.staticOutput,
uiType: block.uiType,
},
};
@@ -550,7 +549,6 @@ const FlowEditor: React.FC<{
[
nodeId,
setViewport,
availableNodes,
addNodes,
nodeDimensions,
deleteElements,
@@ -632,12 +630,12 @@ const FlowEditor: React.FC<{
const editorControls: Control[] = [
{
label: "Undo",
icon: <IconUndo2 />,
icon: <IconUndo2 className="h-5 w-5" strokeWidth={2} />,
onClick: handleUndo,
},
{
label: "Redo",
icon: <IconRedo2 />,
icon: <IconRedo2 className="h-5 w-5" strokeWidth={2} />,
onClick: handleRedo,
},
];
@@ -685,15 +683,13 @@ const FlowEditor: React.FC<{
<Controls />
<Background className="dark:bg-slate-800" />
<ControlPanel
className="absolute z-20"
controls={editorControls}
topChildren={
<BlocksControl
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
blocks={availableNodes}
addBlock={addNode}
flows={availableFlows}
nodes={nodes}
<BlockMenu
pinBlocksPopover={pinBlocksPopover}
addNode={addNode}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>
}
botChildren={
@@ -706,6 +702,8 @@ const FlowEditor: React.FC<{
agentName={agentName}
onNameChange={setAgentName}
pinSavePopover={pinSavePopover}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>
}
></ControlPanel>

View File

@@ -0,0 +1,182 @@
"use client";
import {
Block,
CredentialsProviderName,
LibraryAgent,
Provider,
StoreAgent,
} from "@/lib/autogpt-server-api";
import { createContext, ReactNode, useContext, useState } from "react";
import { convertLibraryAgentIntoBlock } from "@/lib/utils";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export type SearchItem = Block | Provider | LibraryAgent | StoreAgent;
export type DefaultStateType =
| "suggestion"
| "all_blocks"
| "input_blocks"
| "action_blocks"
| "output_blocks"
| "integrations"
| "marketplace_agents"
| "my_agents";
export type CategoryKey =
| "blocks"
| "integrations"
| "marketplace_agents"
| "my_agents";
export interface Filters {
categories: {
blocks: boolean;
integrations: boolean;
marketplace_agents: boolean;
my_agents: boolean;
providers: boolean;
};
createdBy: string[];
}
export type CategoryCounts = Record<CategoryKey, number>;
interface BlockMenuContextType {
defaultState: DefaultStateType;
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
integration: CredentialsProviderName | null;
setIntegration: React.Dispatch<
React.SetStateAction<CredentialsProviderName | null>
>;
searchQuery: string;
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
searchId: string | undefined;
setSearchId: React.Dispatch<React.SetStateAction<string | undefined>>;
filters: Filters;
setFilters: React.Dispatch<React.SetStateAction<Filters>>;
searchData: SearchItem[];
setSearchData: React.Dispatch<React.SetStateAction<SearchItem[]>>;
categoryCounts: CategoryCounts;
setCategoryCounts: React.Dispatch<React.SetStateAction<CategoryCounts>>;
addNode: (block: Block) => void;
handleAddStoreAgent: ({
creator_name,
slug,
}: {
creator_name: string;
slug: string;
}) => Promise<void>;
loadingSlug: string | null;
setLoadingSlug: React.Dispatch<React.SetStateAction<string | null>>;
}
export const BlockMenuContext = createContext<BlockMenuContextType>(
{} as BlockMenuContextType,
);
interface BlockMenuStateProviderProps {
children: ReactNode;
addNode: (block: Block) => void;
}
export function BlockMenuStateProvider({
children,
addNode,
}: BlockMenuStateProviderProps) {
const [defaultState, setDefaultState] =
useState<DefaultStateType>("suggestion");
const [integration, setIntegration] =
useState<CredentialsProviderName | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [filters, setFilters] = useState<Filters>({
categories: {
blocks: false,
integrations: false,
marketplace_agents: false,
my_agents: false,
providers: false,
},
createdBy: [],
});
const [searchData, setSearchData] = useState<SearchItem[]>([]);
const [searchId, setSearchId] = useState<string | undefined>(undefined);
const [categoryCounts, setCategoryCounts] = useState<CategoryCounts>({
blocks: 0,
integrations: 0,
marketplace_agents: 0,
my_agents: 0,
});
const [loadingSlug, setLoadingSlug] = useState<string | null>(null);
const api = useBackendAPI();
const handleAddStoreAgent = async ({
creator_name,
slug,
}: {
creator_name: string;
slug: string;
}) => {
try {
setLoadingSlug(slug);
const details = await api.getStoreAgent(creator_name, slug);
if (!details.active_version_id) {
console.error(
"Cannot add store agent to library: active version ID is missing or undefined",
);
return;
}
const libraryAgent = await api.addMarketplaceAgentToLibrary(
details.active_version_id,
);
const block = convertLibraryAgentIntoBlock(libraryAgent);
addNode(block);
} catch (error) {
console.error("Failed to add store agent:", error);
} finally {
setLoadingSlug(null);
}
};
return (
<BlockMenuContext.Provider
value={{
defaultState,
setDefaultState,
integration,
setIntegration,
searchQuery,
setSearchQuery,
searchId,
setSearchId,
filters,
setFilters,
searchData,
setSearchData,
categoryCounts,
setCategoryCounts,
addNode,
handleAddStoreAgent,
loadingSlug,
setLoadingSlug,
}}
>
{children}
</BlockMenuContext.Provider>
);
}
export function useBlockMenuContext(): BlockMenuContextType {
const context = useContext(BlockMenuContext);
if (!context) {
throw new Error("Error in context of Block");
}
return context;
}

View File

@@ -0,0 +1,8 @@
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const ActionBlocksContent: React.FC = () => {
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
};
export default ActionBlocksContent;

View File

@@ -0,0 +1,163 @@
import React, { useState, useEffect, Fragment, useCallback } from "react";
import Block from "../Block";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { BlockCategoryResponse } from "@/lib/autogpt-server-api";
import { useBlockMenuContext } from "../block-menu-provider";
import ErrorState from "../ErrorState";
import { beautifyString } from "@/lib/utils";
const AllBlocksContent: React.FC = () => {
const { addNode } = useBlockMenuContext();
const [categories, setCategories] = useState<BlockCategoryResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [loadingCategories, setLoadingCategories] = useState<Set<string>>(
new Set(),
);
const api = useBackendAPI();
const fetchBlocks = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await api.getBlockCategories();
setCategories(response);
} catch (err) {
console.error("Failed to fetch block categories:", err);
setError(
err instanceof Error ? err.message : "Failed to load block categories",
);
} finally {
setLoading(false);
}
}, [api]);
useEffect(() => {
fetchBlocks();
}, [api, fetchBlocks]);
const fetchMoreBlockOfACategory = async (category: string) => {
try {
setLoadingCategories((prev) => new Set(prev).add(category));
const response = await api.getBuilderBlocks({ category: category });
const updatedCategories = categories.map((cat) => {
if (cat.name === category) {
return {
...cat,
blocks: [...response.blocks],
};
}
return cat;
});
setCategories(updatedCategories);
} catch (error) {
console.error(`Failed to fetch blocks for category ${category}:`, error);
} finally {
setLoadingCategories((prev) => {
const newSet = new Set(prev);
newSet.delete(category);
return newSet;
});
}
};
if (loading) {
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 3 }).map((_, categoryIndex) => (
<Fragment key={categoryIndex}>
{categoryIndex > 0 && (
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
)}
{[0, 1, 2].map((blockIndex) => (
<Block.Skeleton key={`${categoryIndex}-${blockIndex}`} />
))}
</Fragment>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load blocks"
error={error}
onRetry={fetchBlocks}
/>
</div>
);
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full space-y-3 px-4 pb-4">
{categories.map((category, index) => (
<Fragment key={category.name}>
{index > 0 && (
<Separator className="h-[1px] w-full text-zinc-300" />
)}
{/* Category Section */}
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
{category.name && beautifyString(category.name)}
</p>
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
{category.total_blocks}
</span>
</div>
<div className="space-y-2">
{category.blocks.map((block, idx) => (
<Block
key={`${category.name}-${idx}`}
title={block.name}
description={block.name}
onClick={() => {
addNode(block);
}}
/>
))}
{loadingCategories.has(category.name) && (
<>
{[0, 1, 2, 3, 4].map((skeletonIndex) => (
<Block.Skeleton
key={`skeleton-${category.name}-${skeletonIndex}`}
/>
))}
</>
)}
{category.total_blocks > category.blocks.length && (
<Button
variant={"link"}
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
disabled={loadingCategories.has(category.name)}
onClick={() => {
fetchMoreBlockOfACategory(category.name);
}}
>
see all
</Button>
)}
</div>
</div>
</Fragment>
))}
</div>
</div>
);
};
export default AllBlocksContent;

View File

@@ -0,0 +1,16 @@
import React from "react";
import BlockMenuSidebar from "./BlockMenuSidebar";
import { Separator } from "@/components/ui/separator";
import BlockMenuDefaultContent from "./BlockMenuDefaultContent";
const BlockMenuDefault: React.FC = () => {
return (
<div className="flex flex-1 overflow-y-auto">
<BlockMenuSidebar />
<Separator className="h-full w-[1px] text-zinc-300" />
<BlockMenuDefaultContent />
</div>
);
};
export default BlockMenuDefault;

View File

@@ -0,0 +1,41 @@
import React from "react";
import SuggestionContent from "./SuggestionContent";
import AllBlocksContent from "./AllBlocksContent";
import IntegrationsContent from "./IntegrationsContent";
import MarketplaceAgentsContent from "./MarketplaceAgentsContent";
import MyAgentsContent from "./MyAgentsContent";
import ActionBlocksContent from "./ActionBlocksContent";
import InputBlocksContent from "./InputBlocksContent";
import OutputBlocksContent from "./OutputBlocksContent";
import { useBlockMenuContext } from "../block-menu-provider";
export interface ActionBlock {
id: number;
title: string;
description: string;
}
export interface BlockListType {
id: number;
title: string;
description: string;
}
const BlockMenuDefaultContent: React.FC = ({}) => {
const { defaultState } = useBlockMenuContext();
return (
<div className="h-full flex-1 overflow-hidden">
{defaultState == "suggestion" && <SuggestionContent />}
{defaultState == "all_blocks" && <AllBlocksContent />}
{defaultState == "input_blocks" && <InputBlocksContent />}
{defaultState == "action_blocks" && <ActionBlocksContent />}
{defaultState == "output_blocks" && <OutputBlocksContent />}
{defaultState == "integrations" && <IntegrationsContent />}
{defaultState == "marketplace_agents" && <MarketplaceAgentsContent />}
{defaultState == "my_agents" && <MyAgentsContent />}
</div>
);
};
export default BlockMenuDefaultContent;

View File

@@ -0,0 +1,119 @@
import React, { useEffect, useState } from "react";
import MenuItem from "../MenuItem";
import { DefaultStateType, useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { CountResponse } from "@/lib/autogpt-server-api";
const BlockMenuSidebar: React.FC = ({}) => {
const { defaultState, setDefaultState, setIntegration } =
useBlockMenuContext();
const [blockCounts, setBlockCounts] = useState<CountResponse | undefined>(
undefined,
);
const api = useBackendAPI();
useEffect(() => {
const fetchBlockCounts = async () => {
try {
const counts = await api.getBlockCounts();
setBlockCounts(counts);
} catch (error) {
console.error("Failed to fetch block counts:", error);
}
};
fetchBlockCounts();
}, [api]);
const topLevelMenuItems = [
{
name: "Suggestion",
type: "suggestion",
},
{
name: "All blocks",
type: "all_blocks",
number: blockCounts?.all_blocks,
},
];
const subMenuItems = [
{
name: "Input blocks",
type: "input_blocks",
number: blockCounts?.input_blocks,
},
{
name: "Action blocks",
type: "action_blocks",
number: blockCounts?.action_blocks,
},
{
name: "Output blocks",
type: "output_blocks",
number: blockCounts?.output_blocks,
},
];
const bottomMenuItems = [
{
name: "Integrations",
type: "integrations",
number: blockCounts?.integrations,
onClick: () => {
setIntegration(null);
setDefaultState("integrations");
},
},
{
name: "Marketplace Agents",
type: "marketplace_agents",
number: blockCounts?.marketplace_agents,
},
{
name: "My Agents",
type: "my_agents",
number: blockCounts?.my_agents,
},
];
return (
<div className="w-fit space-y-2 px-4 pt-4">
{topLevelMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
))}
<div className="ml-[0.5365rem] space-y-2 border-l border-black/10 pl-[0.75rem]">
{subMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
className="max-w-[11.5339rem]"
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
))}
</div>
{bottomMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
selected={defaultState === item.type}
onClick={
item.onClick ||
(() => setDefaultState(item.type as DefaultStateType))
}
/>
))}
</div>
);
};
export default BlockMenuSidebar;

View File

@@ -0,0 +1,33 @@
import React from "react";
import Block from "../Block";
import { Block as BlockType } from "@/lib/autogpt-server-api";
import { useBlockMenuContext } from "../block-menu-provider";
interface BlocksListProps {
blocks: BlockType[];
loading?: boolean;
}
const BlocksList: React.FC<BlocksListProps> = ({ blocks, loading = false }) => {
const { addNode } = useBlockMenuContext();
return (
<div className="w-full space-y-3 px-4 pb-4">
{loading
? Array.from({ length: 7 }).map((_, index) => (
<Block.Skeleton key={index} />
))
: blocks.map((block) => (
<Block
key={block.id}
title={block.name}
description={block.description}
onClick={() => {
addNode(block);
}}
/>
))}
</div>
);
};
export default BlocksList;

View File

@@ -0,0 +1,8 @@
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const InputBlocksContent: React.FC = () => {
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
};
export default InputBlocksContent;

View File

@@ -0,0 +1,112 @@
import { Button } from "@/components/ui/button";
import React, { useState, useEffect, Fragment, useCallback } from "react";
import IntegrationBlock from "../IntegrationBlock";
import { useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Block } from "@/lib/autogpt-server-api";
import ErrorState from "../ErrorState";
import { Skeleton } from "@/components/ui/skeleton";
const IntegrationBlocks: React.FC = ({}) => {
const { integration, setIntegration, addNode } = useBlockMenuContext();
const [blocks, setBlocks] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const api = useBackendAPI();
const fetchBlocks = useCallback(async () => {
if (integration) {
try {
setLoading(true);
setError(null);
const response = await api.getBuilderBlocks({ provider: integration });
setBlocks(response.blocks);
} catch (err) {
console.error("Failed to fetch integration blocks:", err);
setError(
err instanceof Error
? err.message
: "Failed to load integration blocks",
);
} finally {
setLoading(false);
}
}
}, [api, integration]);
useEffect(() => {
fetchBlocks();
}, [api, integration, fetchBlocks]);
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{Array.from({ length: 3 }).map((_, blockIndex) => (
<Fragment key={blockIndex}>
{blockIndex > 0 && (
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
)}
{[0, 1, 2].map((index) => (
<IntegrationBlock.Skeleton key={`${blockIndex}-${index}`} />
))}
</Fragment>
))}
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load integration blocks"
error={error}
onRetry={fetchBlocks}
/>
</div>
);
}
return (
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Button
variant={"link"}
className="p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800"
onClick={() => {
setIntegration(null);
}}
>
Integrations
</Button>
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
/
</p>
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
{integration}
</p>
</div>
<span className="flex h-[1.375rem] w-[1.6875rem] items-center justify-center rounded-[1.25rem] bg-[#f0f0f0] p-1.5 font-sans text-sm leading-[1.375rem] text-zinc-500 group-disabled:text-zinc-400">
{blocks.length}
</span>
</div>
<div className="space-y-3">
{blocks.map((block, index) => (
<IntegrationBlock
key={index}
title={block.name}
description={block.description}
icon_url={`/integrations/${integration}.png`}
onClick={() => {
addNode(block);
}}
/>
))}
</div>
</div>
);
};
export default IntegrationBlocks;

View File

@@ -0,0 +1,22 @@
import React from "react";
import PaginatedIntegrationList from "./PaginatedIntegrationList";
import IntegrationBlocks from "./IntegrationBlocks";
import { useBlockMenuContext } from "../block-menu-provider";
const IntegrationsContent: React.FC = () => {
const { integration } = useBlockMenuContext();
if (!integration) {
return <PaginatedIntegrationList />;
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full px-4 pb-4">
<IntegrationBlocks />
</div>
</div>
);
};
export default IntegrationsContent;

View File

@@ -0,0 +1,84 @@
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
import { useBlockMenuContext } from "../block-menu-provider";
const MarketplaceAgentsContent: React.FC = () => {
const {
data: agents,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "store-agents" },
pageSize: 10,
});
const { handleAddStoreAgent, loadingSlug } = useBlockMenuContext();
if (loading) {
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 5 }).map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={index} />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load marketplace agents"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{agents.map((agent) => (
<MarketplaceAgentBlock
key={agent.slug}
slug={agent.slug}
title={agent.agent_name}
image_url={agent.agent_image}
creator_name={agent.creator}
number_of_runs={agent.runs}
loading={loadingSlug === agent.slug}
onClick={() =>
handleAddStoreAgent({
creator_name: agent.creator,
slug: agent.slug,
})
}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);
};
export default MarketplaceAgentsContent;

View File

@@ -0,0 +1,81 @@
import React from "react";
import UGCAgentBlock from "../UGCAgentBlock";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
import { useBlockMenuContext } from "../block-menu-provider";
import { convertLibraryAgentIntoBlock } from "@/lib/utils";
const MyAgentsContent: React.FC = () => {
const {
data: agents,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "library-agents" },
pageSize: 10,
});
const { addNode } = useBlockMenuContext();
if (loading) {
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 5 }).map((_, index) => (
<UGCAgentBlock.Skeleton key={index} />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load library agents"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{agents.map((agent) => (
<UGCAgentBlock
key={agent.id}
title={agent.name}
edited_time={agent.updated_at}
version={agent.graph_version}
image_url={agent.image_url}
onClick={() => {
const block = convertLibraryAgentIntoBlock(agent);
addNode(block);
}}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<UGCAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);
};
export default MyAgentsContent;

View File

@@ -0,0 +1,8 @@
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const OutputBlocksContent: React.FC = () => {
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
};
export default OutputBlocksContent;

View File

@@ -0,0 +1,59 @@
import React, { Fragment } from "react";
import BlocksList from "./BlocksList";
import Block from "../Block";
import { BlockRequest } from "@/lib/autogpt-server-api";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
interface PaginatedBlocksContentProps {
blockRequest: BlockRequest;
pageSize?: number;
}
const PaginatedBlocksContent: React.FC<PaginatedBlocksContentProps> = ({
blockRequest,
pageSize = 10,
}) => {
const {
data: blocks,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "blocks", ...blockRequest },
pageSize,
});
if (error) {
return (
<div className="h-full w-full px-4 pb-4">
<ErrorState
title="Failed to load blocks"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<BlocksList blocks={blocks} loading={loading} />
{loadingMore && hasMore && (
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 3 }).map((_, index) => (
<Block.Skeleton key={`loading-${index}`} />
))}
</div>
)}
</div>
);
};
export default PaginatedBlocksContent;

View File

@@ -0,0 +1,79 @@
import React from "react";
import Integration from "../Integration";
import { useBlockMenuContext } from "../block-menu-provider";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
const PaginatedIntegrationList: React.FC = () => {
const { setIntegration } = useBlockMenuContext();
const {
data: providers,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "providers" },
pageSize: 10,
});
if (loading) {
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 6 }).map((_, integrationIndex) => (
<Integration.Skeleton key={integrationIndex} />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load integrations"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full px-4 pb-4">
<div className="space-y-3">
{providers.map((integration, index) => (
<Integration
key={index}
title={integration.name}
icon_url={`/integrations/${integration.name}.png`}
description={integration.description}
number_of_blocks={integration.integration_count}
onClick={() => setIntegration(integration.name)}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<Integration.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
</div>
);
};
export default PaginatedIntegrationList;

View File

@@ -0,0 +1,160 @@
import React, { useCallback, useEffect, useState } from "react";
import SearchHistoryChip from "../SearchHistoryChip";
import IntegrationChip from "../IntegrationChip";
import Block from "../Block";
import { useBlockMenuContext } from "../block-menu-provider";
import {
CredentialsProviderName,
SuggestionsResponse,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import ErrorState from "../ErrorState";
const SuggestionContent: React.FC = () => {
const { setIntegration, setDefaultState, setSearchQuery, addNode } =
useBlockMenuContext();
const [suggestionsData, setSuggestionsData] =
useState<SuggestionsResponse | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const api = useBackendAPI();
const fetchSuggestions = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await api.getSuggestions();
setSuggestionsData(response);
} catch (err) {
console.error("Error fetching data:", err);
setError(
err instanceof Error ? err.message : "Failed to load suggestions",
);
} finally {
setLoading(false);
}
}, [api]);
useEffect(() => {
fetchSuggestions();
}, [api, fetchSuggestions]);
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load suggestions"
error={error}
onRetry={fetchSuggestions}
/>
</div>
);
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full space-y-6 pb-4">
{/* Recent Searches */}
{/* <div className="-mb-2 space-y-2.5">
<p className="px-4 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Recent searches
</p>
<div className="scrollbar-thumb-rounded flex flex-nowrap gap-2 overflow-x-auto transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
{" "}
{!loading && suggestionsData
? suggestionsData.recent_searches.map((search, index) => (
<SearchHistoryChip
key={`search-${index}`}
content={search}
className={index === 0 ? "ml-4" : ""}
onClick={() => setSearchQuery(search)}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<SearchHistoryChip.Skeleton
key={`search-${index}`}
className={index === 0 ? "ml-4" : ""}
/>
))}
{!loading && suggestionsData
? suggestionsData.recent_searches.map((search, index) => (
<SearchHistoryChip
key={`search-${index}`}
content={search}
className={index === 0 ? "ml-4" : ""}
onClick={() => setSearchQuery(search)}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<SearchHistoryChip.Skeleton
key={`search-${index}`}
className={index === 0 ? "ml-4" : ""}
/>
))}
</div>
</div> */}
{/* Integrations */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Integrations
</p>
<div className="grid grid-cols-3 grid-rows-2 gap-2">
{!loading && suggestionsData
? suggestionsData.providers.map((provider, index) => (
<IntegrationChip
key={`integration-${index}`}
icon_url={`/integrations/${provider}.png`}
name={provider}
onClick={() => {
setDefaultState("integrations");
setIntegration(provider as CredentialsProviderName);
}}
/>
))
: Array(6)
.fill(0)
.map((_, index) => (
<IntegrationChip.Skeleton
key={`integration-skeleton-${index}`}
/>
))}
</div>
</div>
{/* Top blocks */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Top blocks
</p>
<div className="space-y-2">
{!loading && suggestionsData
? suggestionsData.top_blocks.map((block, index) => (
<Block
key={`block-${index}`}
title={block.name}
description={block.description}
onClick={() => {
addNode(block);
}}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<Block.Skeleton key={`block-skeleton-${index}`} />
))}
</div>
</div>
</div>
</div>
);
};
export default SuggestionContent;

View File

@@ -0,0 +1,64 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Plus } from "lucide-react";
import { ButtonHTMLAttributes } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
description?: string;
ai_name?: string;
}
const AiBlock: React.FC<Props> = ({
title,
description,
className,
ai_name,
...rest
}) => {
return (
<Button
className={cn(
"group flex h-[5.625rem] w-full min-w-[7.5rem] items-center justify-start space-x-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
"hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
)}
{...rest}
>
<div className="flex flex-1 flex-col items-start gap-1.5">
<div className="space-y-0.5">
<span
className={cn(
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-700 group-disabled:text-zinc-400",
)}
>
{title}
</span>
<span
className={cn(
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
{description}
</span>
</div>
<span
className={cn(
"rounded-[0.75rem] bg-zinc-200 px-[0.5rem] font-sans text-xs leading-[1.25rem] text-zinc-500",
)}
>
Supports {ai_name}
</span>
</div>
<div
className={cn(
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
)}
>
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
</div>
</Button>
);
};
export default AiBlock;

View File

@@ -0,0 +1,147 @@
import React, { useEffect, useState, useCallback, useRef } from "react";
import FiltersList from "./FiltersList";
import SearchList from "./SearchList";
import { useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const BlockMenuSearch: React.FC = ({}) => {
const {
searchData,
searchQuery,
searchId,
setSearchData,
filters,
setCategoryCounts,
} = useBlockMenuContext();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(true);
const [page, setPage] = useState<number>(1);
const [loadingMore, setLoadingMore] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const api = useBackendAPI();
const pageSize = 10;
const fetchSearchData = useCallback(
async (pageNum: number, isLoadMore: boolean = false) => {
if (isLoadMore) {
setLoadingMore(true);
} else {
setIsLoading(true);
}
try {
const activeCategories = Object.entries(filters.categories)
.filter(([_, isActive]) => isActive)
.map(([category, _]) => category)
.map(
(category) =>
category as
| "blocks"
| "integrations"
| "marketplace_agents"
| "my_agents",
);
const response = await api.searchBlocks({
search_query: searchQuery,
search_id: searchId,
page: pageNum,
page_size: pageSize,
filter: activeCategories.length > 0 ? activeCategories : undefined,
by_creator:
filters.createdBy.length > 0 ? filters.createdBy : undefined,
});
setCategoryCounts(response.total_items);
if (isLoadMore) {
setSearchData((prev) => [...prev, ...response.items]);
} else {
setSearchData(response.items);
}
setHasMore(response.more_pages);
setError(null);
} catch (error) {
console.error("Error fetching search data:", error);
setError(
error instanceof Error
? error.message
: "Failed to load search results",
);
if (!isLoadMore) {
setPage(1);
}
} finally {
setIsLoading(false);
setLoadingMore(false);
}
},
[
searchQuery,
searchId,
filters,
api,
setCategoryCounts,
setSearchData,
pageSize,
],
);
const handleScroll = useCallback(() => {
if (!scrollRef.current || loadingMore || !hasMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
if (scrollTop + clientHeight >= scrollHeight - 100) {
const nextPage = page + 1;
setPage(nextPage);
fetchSearchData(nextPage, true);
}
}, [loadingMore, hasMore, page, fetchSearchData]);
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement) {
scrollElement.addEventListener("scroll", handleScroll);
return () => scrollElement.removeEventListener("scroll", handleScroll);
}
}, [handleScroll]);
useEffect(() => {
if (searchQuery) {
setPage(1);
setHasMore(true);
setError(null);
fetchSearchData(1, false);
} else {
setSearchData([]);
setError(null);
setPage(1);
setHasMore(true);
}
}, [searchQuery, searchId, filters, fetchSearchData, setSearchData]);
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full space-y-4 overflow-y-auto py-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
{searchData.length !== 0 && <FiltersList />}
<SearchList
isLoading={isLoading}
loadingMore={loadingMore}
hasMore={hasMore}
error={error}
onRetry={() => {
setPage(1);
setError(null);
fetchSearchData(1, false);
}}
/>
</div>
);
};
export default BlockMenuSearch;

View File

@@ -0,0 +1,267 @@
import FilterChip from "../FilterChip";
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { cn, getBlockType } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import {
CategoryKey,
Filters,
useBlockMenuContext,
} from "../block-menu-provider";
import { StoreAgent } from "@/lib/autogpt-server-api";
const INITIAL_CREATORS_TO_SHOW = 5;
export default function FilterSheet({
categories,
}: {
categories: Array<{ key: CategoryKey; name: string }>;
}) {
const { filters, setFilters, searchData } = useBlockMenuContext();
const [isOpen, setIsOpen] = useState(false);
const [isSheetVisible, setIsSheetVisible] = useState(false);
const [localFilters, setLocalFilters] = useState<Filters>(filters);
const [creators, setCreators] = useState<string[]>([]);
const [displayedCreatorsCount, setDisplayedCreatorsCount] = useState(
INITIAL_CREATORS_TO_SHOW,
);
useEffect(() => {
if (isOpen) {
setIsSheetVisible(true);
setLocalFilters(filters);
setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW); // Reset on open
const marketplaceAgents = (searchData?.filter(
(item) => getBlockType(item) === "store_agent",
) || []) as StoreAgent[];
const uniqueCreators = Array.from(
new Set(marketplaceAgents.map((agent) => agent.creator)),
);
setCreators(uniqueCreators);
} else {
const timer = setTimeout(() => {
setIsSheetVisible(false);
}, 300);
return () => clearTimeout(timer);
}
}, [isOpen, filters, searchData]);
const onCategoryChange = useCallback((category: CategoryKey) => {
setLocalFilters((prev) => ({
...prev,
categories: {
...prev.categories,
[category]: !prev.categories[category],
},
}));
}, []);
const onCreatorChange = useCallback((creator: string) => {
setLocalFilters((prev) => {
const updatedCreators = prev.createdBy.includes(creator)
? prev.createdBy.filter((c) => c !== creator)
: [...prev.createdBy, creator];
return {
...prev,
createdBy: updatedCreators,
};
});
}, []);
const handleApplyFilters = useCallback(() => {
setFilters(localFilters);
setIsOpen(false);
}, [localFilters, setFilters]);
const handleClearFilters = useCallback(() => {
const clearedFilters: Filters = {
categories: {
blocks: false,
integrations: false,
marketplace_agents: false,
my_agents: false,
providers: false,
},
createdBy: [],
};
setFilters(clearedFilters);
setIsOpen(false);
}, [setFilters]);
const hasLocalActiveFilters = useCallback(() => {
const hasCategoryFilter = Object.values(localFilters.categories).some(
(value) => value,
);
const hasCreatorFilter = localFilters.createdBy.length > 0;
return hasCategoryFilter || hasCreatorFilter;
}, [localFilters]);
const hasActiveFilters = useCallback(() => {
const hasCategoryFilter = Object.values(filters.categories).some(
(value) => value,
);
const hasCreatorFilter = filters.createdBy.length > 0;
return hasCategoryFilter || hasCreatorFilter;
}, [filters]);
const handleToggleShowMoreCreators = () => {
if (displayedCreatorsCount < creators.length) {
setDisplayedCreatorsCount(creators.length);
} else {
setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW);
}
};
const visibleCreators = creators.slice(0, displayedCreatorsCount);
return (
<div className="m-0 ml-4 inline w-fit p-0">
<Button
onClick={() => {
setIsSheetVisible(true);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsOpen(true);
});
});
}}
variant={"link"}
className="m-0 p-0 hover:no-underline"
>
<FilterChip
name={hasActiveFilters() ? "Edit filters" : "All filters"}
/>
</Button>
{isSheetVisible && (
<>
<div
className={cn(
"absolute bottom-2 left-2 top-2 z-20 w-3/4 max-w-[22.5rem] space-y-4 overflow-hidden rounded-[0.75rem] bg-white pb-4 shadow-[0_4px_12px_2px_rgba(0,0,0,0.1)] transition-all",
isOpen
? "translate-x-0 duration-300 ease-out"
: "-translate-x-full duration-300 ease-out",
)}
>
<div
className={cn(
"flex-1 space-y-4 pb-16",
"scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200",
)}
>
{/* Top */}
<div className="flex items-center justify-between px-5">
<p className="font-sans text-base text-[#040404]">Filters</p>
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(false)}
>
<X className="h-5 w-5" />
</Button>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Categories */}
<div className="space-y-4 px-5">
<p className="font-sans text-base font-medium text-zinc-800">
Categories
</p>
<div className="space-y-2">
{categories.map((category) => (
<div
key={category.key}
className="flex items-center space-x-2"
>
<Checkbox
id={category.key}
checked={localFilters.categories[category.key]}
onCheckedChange={() => onCategoryChange(category.key)}
className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white"
/>
<label
htmlFor={category.key}
className="font-sans text-sm leading-[1.375rem] text-zinc-600"
>
{category.name}
</label>
</div>
))}
</div>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Created By */}
<div className="space-y-4 px-5">
<p className="font-sans text-base font-medium text-zinc-800">
Created by
</p>
<div className="space-y-2">
{visibleCreators.map((creator) => (
<div key={creator} className="flex items-center space-x-2">
<Checkbox
id={`creator-${creator}`}
checked={localFilters.createdBy.includes(creator)}
onCheckedChange={() => onCreatorChange(creator)}
className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white"
/>
<label
htmlFor={`creator-${creator}`}
className="font-sans text-sm leading-[1.375rem] text-zinc-600"
>
{creator}
</label>
</div>
))}
</div>
{creators.length > INITIAL_CREATORS_TO_SHOW && (
<Button
variant={"link"}
className="m-0 p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 underline hover:text-zinc-600"
onClick={handleToggleShowMoreCreators}
>
{displayedCreatorsCount < creators.length ? "More" : "Less"}
</Button>
)}
</div>
</div>
{/* Footer buttons */}
<div className="fixed bottom-0 flex w-full justify-between gap-3 border-t border-zinc-300 bg-white px-5 py-3">
<Button
className="min-w-[5rem] rounded-[0.5rem] border-none px-1.5 py-2 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 shadow-none ring-1 ring-zinc-400"
variant={"outline"}
onClick={handleClearFilters}
>
Clear
</Button>
<Button
className={cn(
"min-w-[6.25rem] rounded-[0.5rem] border-none px-1.5 py-2 font-sans text-sm font-medium leading-[1.375rem] text-white shadow-none ring-1 disabled:ring-0",
)}
onClick={handleApplyFilters}
disabled={!hasLocalActiveFilters()}
>
Apply filters
</Button>
</div>
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { useCallback } from "react";
import FilterChip from "../FilterChip";
import FilterSheet from "./FilterSheet";
import { CategoryKey, useBlockMenuContext } from "../block-menu-provider";
const FiltersList = () => {
const { filters, setFilters, categoryCounts } = useBlockMenuContext();
const categories: Array<{ key: CategoryKey; name: string }> = [
{ key: "blocks", name: "Blocks" },
{ key: "integrations", name: "Integrations" },
{ key: "marketplace_agents", name: "Marketplace agents" },
{ key: "my_agents", name: "My agents" },
];
const handleCategoryFilter = (category: CategoryKey) => {
setFilters({
...filters,
categories: {
...filters.categories,
[category]: !filters.categories[category],
},
});
};
const handleCreatorFilter = useCallback(
(creator: string) => {
const updatedCreators = filters.createdBy.includes(creator)
? filters.createdBy.filter((c) => c !== creator)
: [...filters.createdBy, creator];
setFilters({
...filters,
createdBy: updatedCreators,
});
},
[filters, setFilters],
);
return (
<div className="flex flex-nowrap gap-3 overflow-x-auto scrollbar-hide">
<FilterSheet categories={categories} />
{filters.createdBy.map((creator) => (
<FilterChip
key={creator}
name={"Created by " + creator}
selected={true}
onClick={() => handleCreatorFilter(creator)}
/>
))}
{categories.map((category) => (
<FilterChip
key={category.key}
name={category.name}
number={categoryCounts[category.key]}
selected={filters.categories[category.key]}
onClick={() => handleCategoryFilter(category.key)}
/>
))}
</div>
);
};
export default FiltersList;

View File

@@ -0,0 +1,19 @@
import { Frown } from "lucide-react";
const NoSearchResult = () => {
return (
<div className="flex h-full w-full flex-col items-center justify-center text-center">
<Frown className="mb-10 h-16 w-16 text-zinc-400" strokeWidth={1} />
<div className="space-y-1">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
No match found
</p>
<p className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-600">
Try adjusting your search terms
</p>
</div>
</div>
);
};
export default NoSearchResult;

View File

@@ -0,0 +1,173 @@
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import Block from "../Block";
import UGCAgentBlock from "../UGCAgentBlock";
import AiBlock from "./AiBlock";
import IntegrationBlock from "../IntegrationBlock";
import { useBlockMenuContext } from "../block-menu-provider";
import NoSearchResult from "./NoSearchResult";
import { Button } from "@/components/ui/button";
import { convertLibraryAgentIntoBlock, getBlockType } from "@/lib/utils";
interface SearchListProps {
isLoading: boolean;
loadingMore: boolean;
hasMore: boolean;
error: string | null;
onRetry: () => void;
}
const SearchList: React.FC<SearchListProps> = ({
isLoading,
loadingMore,
hasMore,
error,
onRetry,
}) => {
const { searchQuery, addNode, loadingSlug, searchData, handleAddStoreAgent } =
useBlockMenuContext();
if (isLoading) {
return (
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Search results
</p>
{Array(6)
.fill(0)
.map((_, i) => (
<Block.Skeleton key={i} />
))}
</div>
);
}
if (error) {
return (
<div className="px-4">
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="mb-2 text-sm text-red-600">
Error loading search results: {error}
</p>
<Button
variant="outline"
size="sm"
onClick={onRetry}
className="h-7 text-xs"
>
Retry
</Button>
</div>
</div>
);
}
if (searchData.length === 0) {
return <NoSearchResult />;
}
return (
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Search results
</p>
{searchData.map((item: any, index: number) => {
const blockType = getBlockType(item);
switch (blockType) {
case "store_agent":
return (
<MarketplaceAgentBlock
key={index}
slug={item.slug}
highlightedText={searchQuery}
title={item.agent_name}
image_url={item.agent_image}
creator_name={item.creator}
number_of_runs={item.runs}
loading={loadingSlug == item.slug}
onClick={() =>
handleAddStoreAgent({
creator_name: item.creator,
slug: item.slug,
})
}
/>
);
case "block":
return (
<Block
key={index}
title={item.name}
highlightedText={searchQuery}
description={item.description}
onClick={() => {
addNode(item);
}}
/>
);
case "provider":
return (
<IntegrationBlock
key={index}
title={item.name}
highlightedText={searchQuery}
icon_url={`/integrations/${item.name}.png`}
description={item.description}
onClick={() => {
addNode(item);
}}
/>
);
case "library_agent":
return (
<UGCAgentBlock
key={index}
title={item.name}
highlightedText={searchQuery}
image_url={item.image_url}
version={item.graph_version}
edited_time={item.updated_at}
onClick={() => {
const block = convertLibraryAgentIntoBlock(item);
addNode(block);
}}
/>
);
case "ai_agent":
return (
<AiBlock
key={index}
title={item.name}
description={item.description}
ai_name={item.inputSchema.properties.model.enum.find(
(model: string) =>
model
.toLowerCase()
.includes(searchQuery.toLowerCase().trim()),
)}
onClick={() => {
const block = convertLibraryAgentIntoBlock(item);
addNode(block);
}}
/>
);
default:
return null;
}
})}
{loadingMore && hasMore && (
<div className="space-y-2.5">
{Array(3)
.fill(0)
.map((_, i) => (
<Block.Skeleton key={`loading-more-${i}`} />
))}
</div>
)}
</div>
);
};
export default SearchList;

View File

@@ -1,13 +1,7 @@
import { Card, CardContent } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import React from "react";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
/**
* Represents a control element for the ControlPanel Component.
@@ -27,6 +21,7 @@ interface ControlPanelProps {
controls: Control[];
topChildren?: React.ReactNode;
botChildren?: React.ReactNode;
className?: string;
}
@@ -45,42 +40,31 @@ export const ControlPanel = ({
className,
}: ControlPanelProps) => {
return (
<Card className={cn("m-4 mt-24 w-14 dark:bg-slate-900", className)}>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-3 rounded-xl py-3">
{topChildren}
<Separator className="dark:bg-slate-700" />
{controls.map((control, index) => (
<Tooltip key={index} delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Button
variant="ghost"
size="icon"
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</div>
</TooltipTrigger>
<TooltipContent
side="right"
className="dark:bg-slate-800 dark:text-slate-100"
>
{control.label}
</TooltipContent>
</Tooltip>
))}
<Separator className="dark:bg-slate-700" />
{botChildren}
</div>
</CardContent>
</Card>
<section
className={cn(
"absolute left-4 top-24 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)]",
className,
)}
>
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
{topChildren}
<Separator className="text-[#E1E1E1]" />
{controls.map((control, index) => (
<ControlPanelButton
key={index}
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="rounded-none"
>
{control.icon}
</ControlPanelButton>
))}
<Separator className="text-[#E1E1E1]" />
{botChildren}
</div>
</section>
);
};
export default ControlPanel;

View File

@@ -16,6 +16,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
interface SaveControlProps {
agentMeta: GraphMeta | null;
@@ -26,6 +27,11 @@ interface SaveControlProps {
onNameChange: (name: string) => void;
onDescriptionChange: (description: string) => void;
pinSavePopover: boolean;
blockMenuSelected: "save" | "block" | "";
setBlockMenuSelected: React.Dispatch<
React.SetStateAction<"" | "save" | "block">
>;
}
/**
@@ -48,6 +54,8 @@ export const SaveControl = ({
onNameChange,
agentDescription,
onDescriptionChange,
blockMenuSelected,
setBlockMenuSelected,
pinSavePopover,
}: SaveControlProps) => {
/**
@@ -82,27 +90,29 @@ export const SaveControl = ({
}, [handleSave, toast]);
return (
<Popover open={pinSavePopover ? true : undefined}>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
data-id="save-control-popover-trigger"
data-testid="blocks-control-save-button"
name="Save"
>
<IconSave className="dark:text-gray-300" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Save</TooltipContent>
</Tooltip>
<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"
>
<IconSave className="h-5 w-5" strokeWidth={2} />
</ControlPanelButton>
</PopoverTrigger>
<PopoverContent
side="right"
sideOffset={15}
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">