mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-13 00:58:16 -05:00
Compare commits
99 Commits
dev
...
abhi/block
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7926919e3e | ||
|
|
27e53aa3dd | ||
|
|
a24673d15f | ||
|
|
9d2d9606e8 | ||
|
|
91407dfc33 | ||
|
|
851919d2d5 | ||
|
|
d6acb02cb6 | ||
|
|
9c07206725 | ||
|
|
4bd3447301 | ||
|
|
8adc9f967d | ||
|
|
349b70c4bc | ||
|
|
9ecfa1e1f1 | ||
|
|
4e17f9c49e | ||
|
|
31fdeeb706 | ||
|
|
e42b24c029 | ||
|
|
2d52a57a21 | ||
|
|
f45123f6b6 | ||
|
|
d524518f41 | ||
|
|
81d1b28d92 | ||
|
|
4e4e754ac1 | ||
|
|
e409d7aa34 | ||
|
|
8312a339c2 | ||
|
|
5b45d246ef | ||
|
|
5c7c7ca874 | ||
|
|
c93c5e35ba | ||
|
|
ce989b1bf7 | ||
|
|
c1c919b88b | ||
|
|
21a91fe9fd | ||
|
|
b2f3d8c1f2 | ||
|
|
46ab2e3b20 | ||
|
|
5b40700299 | ||
|
|
1a97020eeb | ||
|
|
39d03f2090 | ||
|
|
8088d294f4 | ||
|
|
31266949ed | ||
|
|
f4eb00a6ad | ||
|
|
f75cc0dd11 | ||
|
|
21b612625f | ||
|
|
eec0d276d5 | ||
|
|
c6941e7f6e | ||
|
|
325684a10f | ||
|
|
cf057cbbda | ||
|
|
f3a7be1fd3 | ||
|
|
97bcb0f95e | ||
|
|
dd71d65706 | ||
|
|
2b2d26bcde | ||
|
|
67f6f43e1b | ||
|
|
a3409c9578 | ||
|
|
7f82457ea4 | ||
|
|
a5c0fabc00 | ||
|
|
09dba93a4a | ||
|
|
ea2cd3e7bf | ||
|
|
d3d0ccf732 | ||
|
|
d8d5d6ec0c | ||
|
|
f45b09c0b5 | ||
|
|
1e89b6d3a4 | ||
|
|
950a85e179 | ||
|
|
c5e3148145 | ||
|
|
a135ba3f0b | ||
|
|
fe95e27226 | ||
|
|
711ca10cc9 | ||
|
|
1346d8230c | ||
|
|
07c84a4757 | ||
|
|
596824c1e7 | ||
|
|
79afa6db99 | ||
|
|
e034c16f31 | ||
|
|
9012eff1ac | ||
|
|
0361ea4aa4 | ||
|
|
6f1c522ea3 | ||
|
|
2d654bf64b | ||
|
|
bb69e32fee | ||
|
|
1be830835b | ||
|
|
a2a4d546f7 | ||
|
|
3053a7bd06 | ||
|
|
bbf4108136 | ||
|
|
95387bcf78 | ||
|
|
e1fc56e6f3 | ||
|
|
2a06956802 | ||
|
|
32231ff80f | ||
|
|
d0b23c085f | ||
|
|
e718d3d3d8 | ||
|
|
1971a62684 | ||
|
|
e125b5923c | ||
|
|
c6942e4e6f | ||
|
|
c9e421a219 | ||
|
|
7868373897 | ||
|
|
f1c8399e0e | ||
|
|
97ba69ef1c | ||
|
|
773e1488bf | ||
|
|
4273be59ba | ||
|
|
06e524788a | ||
|
|
bc08012771 | ||
|
|
4af0aedebd | ||
|
|
d22464a75e | ||
|
|
82e3a485f0 | ||
|
|
8165ad5879 | ||
|
|
451284de76 | ||
|
|
1d8c7c5e1a | ||
|
|
34be6a3379 |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||
|
||||
const ActionBlocksContent: React.FC = () => {
|
||||
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
|
||||
};
|
||||
|
||||
export default ActionBlocksContent;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||
|
||||
const InputBlocksContent: React.FC = () => {
|
||||
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
|
||||
};
|
||||
|
||||
export default InputBlocksContent;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||
|
||||
const OutputBlocksContent: React.FC = () => {
|
||||
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
|
||||
};
|
||||
|
||||
export default OutputBlocksContent;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user