Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update

This commit is contained in:
Krzysztof Czerwinski
2025-05-29 13:16:18 +02:00
59 changed files with 1194 additions and 1091 deletions

View File

@@ -80,6 +80,7 @@
"react-markdown": "^9.0.3",
"react-modal": "^3.16.3",
"react-shepherd": "^6.1.8",
"react-timeago": "^8.2.0",
"recharts": "^2.15.3",
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar": "^4.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -27,6 +27,7 @@ import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import {
Block,
BlockUIType,
formatEdgeID,
GraphExecutionID,
@@ -467,13 +468,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.
@@ -490,7 +485,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,
)
@@ -505,19 +500,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,
},
};
@@ -546,7 +541,6 @@ const FlowEditor: React.FC<{
[
nodeId,
setViewport,
availableNodes,
addNodes,
nodeDimensions,
deleteElements,
@@ -685,7 +679,7 @@ const FlowEditor: React.FC<{
topChildren={
<BlockMenu
pinBlocksPopover={pinBlocksPopover}
addBlock={addNode}
addNode={addNode}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>

View File

@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { beautifyString, cn } from "@/lib/utils";
import { Plus } from "lucide-react";
import React, { ButtonHTMLAttributes } from "react";
import { highlightText } from "./IntegrationBlock";
@@ -26,7 +26,7 @@ const Block: BlockComponent = ({
<Button
className={cn(
"group flex h-16 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",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
)}
{...rest}
>
@@ -36,7 +36,7 @@ const Block: BlockComponent = ({
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
)}
>
{highlightText(title, highlightedText)}
{title && highlightText(beautifyString(title), highlightedText)}
</span>
<span
className={cn(

View File

@@ -10,13 +10,10 @@ import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButt
import { ToyBrick } from "lucide-react";
import BlockMenuContent from "./BlockMenuContent";
import { BlockMenuStateProvider } from "./block-menu-provider";
import { Block } from "@/lib/autogpt-server-api";
interface BlockMenuProps {
addBlock: (
id: string,
name: string,
hardcodedValues: Record<string, any>,
) => void;
addNode: (block: Block) => void;
pinBlocksPopover: boolean;
blockMenuSelected: "save" | "block" | "";
setBlockMenuSelected: React.Dispatch<
@@ -25,7 +22,7 @@ interface BlockMenuProps {
}
export const BlockMenu: React.FC<BlockMenuProps> = ({
addBlock,
addNode,
pinBlocksPopover,
blockMenuSelected,
setBlockMenuSelected,
@@ -61,7 +58,7 @@ export const BlockMenu: React.FC<BlockMenuProps> = ({
className="absolute h-[75vh] w-[46.625rem] overflow-hidden rounded-[1rem] border-none p-0 shadow-[0_2px_6px_0_rgba(0,0,0,0.05)]"
data-id="blocks-control-popover-content"
>
<BlockMenuStateProvider>
<BlockMenuStateProvider addNode={addNode}>
<BlockMenuContent />
</BlockMenuStateProvider>
</PopoverContent>

View File

@@ -1,8 +1,10 @@
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { Search } from "lucide-react";
import React, { useRef } from "react";
import { Search, X } from "lucide-react";
import React, { useRef, useState, useEffect, useMemo } from "react";
import { useBlockMenuContext } from "./block-menu-provider";
import { Button } from "@/components/ui/button";
import debounce from "lodash/debounce";
import { Input } from "@/components/ui/input";
interface BlockMenuSearchBarProps {
className?: string;
@@ -11,8 +13,37 @@ interface BlockMenuSearchBarProps {
const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
className = "",
}) => {
const inputRef = useRef(null);
const { searchQuery, setSearchQuery } = useBlockMenuContext();
const inputRef = useRef<HTMLInputElement>(null);
const [localQuery, setLocalQuery] = useState("");
const { searchQuery, setSearchQuery, searchId, setSearchId } =
useBlockMenuContext();
const debouncedSetSearchQuery = useMemo(
() =>
debounce((value: string) => {
setSearchQuery(value);
if (value.length === 0) {
setSearchId(undefined);
} else if (!searchId) {
setSearchId(crypto.randomUUID());
}
}, 500),
[setSearchQuery, setSearchId, searchId],
);
useEffect(() => {
return () => {
debouncedSetSearchQuery.cancel();
};
}, [debouncedSetSearchQuery]);
const handleClear = () => {
setLocalQuery("");
setSearchQuery("");
setSearchId(undefined);
debouncedSetSearchQuery.cancel();
};
return (
<div
className={cn(
@@ -24,9 +55,10 @@ const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
<Input
ref={inputRef}
type="text"
value={searchQuery}
value={localQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setLocalQuery(e.target.value);
debouncedSetSearchQuery(e.target.value);
}}
placeholder={"Blocks, Agents, Integrations or Keywords..."}
className={cn(
@@ -34,6 +66,16 @@ const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
"placeholder:text-zinc-400 focus:shadow-none focus:outline-none focus:ring-0",
)}
/>
{localQuery.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleClear}
className="h-6 w-6 p-0 hover:bg-zinc-100"
>
<X className="h-4 w-4 text-zinc-500" />
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,58 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AlertCircle, RefreshCw } from "lucide-react";
import React from "react";
interface ErrorStateProps {
title?: string;
message?: string;
error?: string | Error | null;
onRetry?: () => void;
retryLabel?: string;
className?: string;
showIcon?: boolean;
}
const ErrorState: React.FC<ErrorStateProps> = ({
title = "Something went wrong",
message,
error,
onRetry,
retryLabel = "Retry",
className,
showIcon = true,
}) => {
const errorMessage = error
? error instanceof Error
? error.message
: String(error)
: message || "An unexpected error occurred. Please try again.";
const classes =
"flex h-full w-full flex-col items-center justify-center text-center space-y-4";
return (
<div className={cn(classes, className)}>
{showIcon && <AlertCircle className="h-12 w-12" strokeWidth={1.5} />}
<div className="space-y-2">
<p className="text-sm font-medium text-zinc-800">{title}</p>
<p className="text-sm text-zinc-600">{errorMessage}</p>
</div>
{onRetry && (
<Button
variant="default"
size="sm"
onClick={onRetry}
className="mt-2 h-7 text-xs"
>
<RefreshCw className="mr-1 h-3 w-3" />
{retryLabel}
</Button>
)}
</div>
);
};
export default ErrorState;

View File

@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { beautifyString, cn } from "@/lib/utils";
import Image from "next/image";
import React, { ButtonHTMLAttributes } from "react";
@@ -27,18 +27,19 @@ const Integration: IntegrationComponent = ({
<Button
className={cn(
"group flex h-16 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-50 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-50 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
className,
)}
{...rest}
>
<div className="relative h-[2.625rem] w-[2.625rem] rounded-[0.5rem] bg-white">
<div className="relative h-[2.625rem] w-[2.625rem] overflow-hidden rounded-[0.5rem] bg-white">
{icon_url && (
<Image
src={icon_url}
alt="integration-icon"
fill
className="w-full object-contain group-disabled:opacity-50"
sizes="2.25rem"
className="w-full rounded-[0.5rem] object-contain group-disabled:opacity-50"
/>
)}
</div>
@@ -46,7 +47,7 @@ const Integration: IntegrationComponent = ({
<div className="w-full">
<div className="flex items-center justify-between gap-2">
<p className="line-clamp-1 flex-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-700 group-disabled:text-zinc-400">
{title}
{title && beautifyString(title)}
</p>
<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">
{number_of_blocks}

View File

@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { beautifyString, cn } from "@/lib/utils";
import { Plus } from "lucide-react";
import Image from "next/image";
import React, { ButtonHTMLAttributes } from "react";
@@ -45,7 +45,7 @@ const IntegrationBlock: IntegrationBlockComponent = ({
<Button
className={cn(
"group flex h-16 w-full min-w-[7.5rem] items-center justify-start gap-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",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
)}
{...rest}
>
@@ -55,6 +55,7 @@ const IntegrationBlock: IntegrationBlockComponent = ({
src={icon_url}
alt="integration-icon"
fill
sizes="2.25rem"
className="w-full object-contain group-disabled:opacity-50"
/>
)}
@@ -65,7 +66,7 @@ const IntegrationBlock: IntegrationBlockComponent = ({
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
)}
>
{highlightText(title, highlightedText)}
{title && highlightText(beautifyString(title), highlightedText)}
</span>
<span
className={cn(

View File

@@ -23,7 +23,7 @@ const IntegrationChip: IntegrationChipComponent = ({
<Button
className={cn(
"flex h-[3.25rem] w-full min-w-[7.5rem] justify-start gap-2 whitespace-normal rounded-[0.5rem] bg-zinc-50 p-2 pr-3 shadow-none",
"hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300",
className,
)}
{...rest}
@@ -34,6 +34,7 @@ const IntegrationChip: IntegrationChipComponent = ({
src={icon_url}
alt="integration-icon"
fill
sizes="2.25rem"
className="w-full object-contain"
/>
)}

View File

@@ -31,7 +31,7 @@ const MarketplaceAgentBlock: MarketplaceAgentBlockComponent = ({
<Button
className={cn(
"group flex h-[4.375rem] w-full min-w-[7.5rem] items-center justify-start gap-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 p-[0.625rem] pr-[0.875rem] 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",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
className,
)}
{...rest}
@@ -42,6 +42,7 @@ const MarketplaceAgentBlock: MarketplaceAgentBlockComponent = ({
src={image_url}
alt="integration-icon"
fill
sizes="5.625rem"
className="w-full object-contain group-disabled:opacity-50"
/>
)}

View File

@@ -21,8 +21,8 @@ const MenuItem: React.FC<Props> = ({
<Button
className={cn(
"flex h-[2.375rem] w-[12.875rem] justify-between whitespace-normal rounded-[0.5rem] bg-transparent p-2 pl-3 shadow-none",
"hover:cursor-pointer hover:bg-transparent focus:ring-0",
selected && "bg-zinc-100 hover:bg-zinc-100",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0",
selected && "bg-zinc-100",
className,
)}
{...rest}

View File

@@ -21,7 +21,7 @@ const SearchHistoryChip: SearchHistoryChipComponent = ({
<Button
className={cn(
"my-[1px] h-[2.25rem] space-x-1 rounded-[1.5rem] bg-zinc-50 p-[0.375rem] pr-[0.625rem] shadow-none",
"hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300",
className,
)}
{...rest}

View File

@@ -5,10 +5,11 @@ import { Plus } from "lucide-react";
import Image from "next/image";
import React, { ButtonHTMLAttributes } from "react";
import { highlightText } from "./IntegrationBlock";
import TimeAgo from "react-timeago";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
edited_time?: string;
edited_time?: Date;
version?: number;
image_url?: string;
highlightedText?: string;
@@ -31,7 +32,7 @@ const UGCAgentBlock: UGCAgentBlockComponent = ({
<Button
className={cn(
"group flex h-[4.375rem] w-full min-w-[7.5rem] items-center justify-start gap-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 p-[0.625rem] pr-[0.875rem] 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",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
className,
)}
{...rest}
@@ -42,6 +43,7 @@ const UGCAgentBlock: UGCAgentBlockComponent = ({
src={image_url}
alt="integration-icon"
fill
sizes="5.625rem"
className="w-full object-contain group-disabled:opacity-50"
/>
)}
@@ -60,8 +62,7 @@ const UGCAgentBlock: UGCAgentBlockComponent = ({
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
{/* BLOCK MENU TODO: We need to create a utility to convert the edit time into relative time (e.g., "2 hours ago") */}
Edited on {edited_time}
Edited {edited_time && <TimeAgo date={edited_time} />}
</span>
<span className="font-sans text-zinc-400"></span>

View File

@@ -1,54 +1,15 @@
"use client";
import {
Block,
CredentialsProviderName,
LibraryAgent,
Provider,
StoreAgent,
} from "@/lib/autogpt-server-api";
import { createContext, ReactNode, useContext, useState } from "react";
interface BaseSearchItem {
type: "marketing_agent" | "integration_block" | "block" | "my_agent" | "ai";
}
interface MarketingAgentItem extends BaseSearchItem {
type: "marketing_agent";
title: string;
image_url: string;
creator_name: string;
number_of_runs: number;
}
interface AIItem extends BaseSearchItem {
type: "ai";
title: string;
description: string;
ai_name: string;
}
interface BlockItem extends BaseSearchItem {
type: "block";
title: string;
description: string;
}
interface IntegrationItem extends BaseSearchItem {
type: "integration_block";
title: string;
description: string;
icon_url: string;
number_of_blocks: number;
}
interface MyAgentItem extends BaseSearchItem {
type: "my_agent";
title: string;
image_url: string;
edited_time: string;
version: number;
}
export type SearchItem =
| MarketingAgentItem
| AIItem
| BlockItem
| IntegrationItem
| MyAgentItem;
export type SearchItem = Block | Provider | LibraryAgent | StoreAgent;
export type DefaultStateType =
| "suggestion"
@@ -63,9 +24,9 @@ export type DefaultStateType =
export type CategoryKey =
| "blocks"
| "integrations"
| "providers"
| "marketplace_agents"
| "my_agents"
| "templates";
| "my_agents";
export interface Filters {
categories: {
@@ -73,34 +34,52 @@ export interface Filters {
integrations: boolean;
marketplace_agents: boolean;
my_agents: boolean;
templates: boolean;
providers: boolean;
};
createdBy: string[];
}
export type CategoryCounts = Record<CategoryKey, number>;
interface BlockMenuContextType {
defaultState: DefaultStateType;
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
integration: string;
setIntegration: React.Dispatch<React.SetStateAction<string>>;
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>>;
creators: string[];
setCreators: React.Dispatch<React.SetStateAction<string[]>>;
searchData: SearchItem[];
setSearchData: React.Dispatch<React.SetStateAction<SearchItem[]>>;
categoryCounts: CategoryCounts;
setCategoryCounts: React.Dispatch<React.SetStateAction<CategoryCounts>>;
addNode: (block: Block) => void;
}
export const BlockMenuContext = createContext<BlockMenuContextType>(
{} as BlockMenuContextType,
);
export function BlockMenuStateProvider({ children }: { children: ReactNode }) {
interface BlockMenuStateProviderProps {
children: ReactNode;
addNode: (block: Block) => void;
}
export function BlockMenuStateProvider({
children,
addNode,
}: BlockMenuStateProviderProps) {
const [defaultState, setDefaultState] =
useState<DefaultStateType>("suggestion");
const [integration, setIntegration] = useState("");
const [integration, setIntegration] =
useState<CredentialsProviderName | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [filters, setFilters] = useState<Filters>({
categories: {
@@ -108,7 +87,7 @@ export function BlockMenuStateProvider({ children }: { children: ReactNode }) {
integrations: false,
marketplace_agents: false,
my_agents: false,
templates: false,
providers: false,
},
createdBy: [],
});
@@ -116,6 +95,16 @@ export function BlockMenuStateProvider({ children }: { children: ReactNode }) {
const [creators, setCreators] = useState<string[]>([]);
const [searchId, setSearchId] = useState<string | undefined>(undefined);
const [categoryCounts, setCategoryCounts] = useState<CategoryCounts>({
blocks: 0,
integrations: 0,
marketplace_agents: 0,
my_agents: 0,
providers: 0,
});
return (
<BlockMenuContext.Provider
value={{
@@ -125,12 +114,17 @@ export function BlockMenuStateProvider({ children }: { children: ReactNode }) {
setIntegration,
searchQuery,
setSearchQuery,
searchId,
setSearchId,
creators,
setCreators,
filters,
setFilters,
searchData,
setSearchData,
categoryCounts,
setCategoryCounts,
addNode,
}}
>
{children}

View File

@@ -1,29 +1,8 @@
import React, { useEffect, useState } from "react";
import { actionBlocksListData } from "../../testing_data";
import { BlockListType } from "./BlockMenuDefaultContent";
import BlocksList from "./BlocksList";
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const ActionBlocksContent: React.FC = () => {
const [blocks, setBlocks] = useState<BlockListType[]>([]);
const [loading, setLoading] = useState(true);
// TEMPORARY FETCHING
useEffect(() => {
const fetchBlocks = async () => {
setLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
setBlocks(actionBlocksListData);
} catch (error) {
console.error("Error fetching blocks:", error);
} finally {
setLoading(false);
}
};
fetchBlocks();
}, []);
return <BlocksList blocks={blocks} loading={loading} />;
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
};
export default ActionBlocksContent;
export default ActionBlocksContent;

View File

@@ -2,36 +2,68 @@ import React, { useState, useEffect, Fragment } from "react";
import Block from "../Block";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { allBlocksDataWithCategories } from "../../testing_data";
import { Skeleton } from "@/components/ui/skeleton";
type BlockItem = {
title: string;
description: string;
};
export type BlockCategory = {
name: string;
count: number;
items: BlockItem[];
};
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";
const AllBlocksContent: React.FC = () => {
const [categories, setCategories] = useState<BlockCategory[]>([]);
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(),
);
// TEMPORARY FETCHING
useEffect(() => {
const fetchBlocks = async () => {
const api = useBackendAPI();
const fetchBlocks = async () => {
try {
setLoading(true);
setTimeout(() => {
setCategories(allBlocksDataWithCategories);
setLoading(false);
}, 800);
};
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);
}
};
useEffect(() => {
fetchBlocks();
}, []);
}, [api]);
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 (
@@ -50,8 +82,20 @@ const AllBlocksContent: React.FC = () => {
);
}
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 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<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}>
@@ -66,23 +110,40 @@ const AllBlocksContent: React.FC = () => {
{category.name}
</p>
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
{category.count}
{category.total_blocks}
</span>
</div>
<div className="space-y-2">
{category.items.slice(0, 3).map((item, idx) => (
{category.blocks.map((block, idx) => (
<Block
key={`${category.name}-${idx}`}
title={item.title}
description={item.description}
title={block.name}
description={block.name}
onClick={() => {
addNode(block);
}}
/>
))}
{category.items.length > 3 && (
{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>

View File

@@ -43,7 +43,7 @@ const BlockMenuSidebar: React.FC = ({}) => {
type: "integrations",
number: 24,
onClick: () => {
setIntegration("");
setIntegration(null);
setDefaultState("integrations");
},
},

View File

@@ -1,28 +1,31 @@
import React from "react";
import Block from "../Block";
import { BlockListType } from "./BlockMenuDefaultContent";
import { Block as BlockType } from "@/lib/autogpt-server-api";
import { useBlockMenuContext } from "../block-menu-provider";
interface BlocksListProps {
blocks: BlockListType[];
blocks: BlockType[];
loading?: boolean;
}
const BlocksList: React.FC<BlocksListProps> = ({ blocks, loading = false }) => {
const { addNode } = useBlockMenuContext();
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<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.title}
description={block.description}
/>
))}
</div>
<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>
);
};

View File

@@ -1,29 +1,8 @@
import React, { useEffect, useState } from "react";
import BlocksList from "./BlocksList";
import { BlockListType } from "./BlockMenuDefaultContent";
import { inputBlocksListData } from "../../testing_data";
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const InputBlocksContent: React.FC = () => {
const [blocks, setBlocks] = useState<BlockListType[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// TEMPORARY FETCHING
const fetchBlocks = async () => {
setLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
setBlocks(inputBlocksListData);
} catch (error) {
console.error("Error fetching blocks:", error);
} finally {
setLoading(false);
}
};
fetchBlocks();
}, []);
return <BlocksList blocks={blocks} loading={loading} />;
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
};
export default InputBlocksContent;
export default InputBlocksContent;

View File

@@ -1,42 +1,73 @@
import { Button } from "@/components/ui/button";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, Fragment } from "react";
import IntegrationBlock from "../IntegrationBlock";
import {
integrationBlocksData,
integrationsListData,
} from "../../testing_data";
import { useBlockMenuContext } from "../block-menu-provider";
export interface IntegrationBlockData {
title: string;
description: string;
icon_url: string;
}
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 } = useBlockMenuContext();
const [blocks, setBlocks] = useState<IntegrationBlockData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const { integration, setIntegration, addNode } = useBlockMenuContext();
const [blocks, setBlocks] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// TEMPORARY FETCHING
useEffect(() => {
const api = useBackendAPI();
const fetchBlocks = async () => {
if (integration) {
setIsLoading(true);
setTimeout(() => {
const foundBlocks = integrationBlocksData[integration] || [];
setBlocks(foundBlocks);
setIsLoading(false);
}, 800);
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);
}
}
}, [integration]);
const getBlockCount = (): number => {
const integrationData = integrationsListData.find(
(item) => item.title === integration,
);
return integrationData?.number_of_blocks || 0;
};
useEffect(() => {
fetchBlocks();
}, [api, integration]);
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{[0, 1, 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">
@@ -45,7 +76,7 @@ const IntegrationBlocks: React.FC = ({}) => {
variant={"link"}
className="p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800"
onClick={() => {
setIntegration("");
setIntegration(null);
}}
>
Integrations
@@ -58,30 +89,22 @@ const IntegrationBlocks: React.FC = ({}) => {
</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">
{getBlockCount()}
{blocks.length}
</span>
</div>
{isLoading ? (
<div className="space-y-3">
{Array(5)
.fill(0)
.map((_, index) => (
<IntegrationBlock.Skeleton key={index} />
))}
</div>
) : (
<div className="space-y-3">
{blocks.map((block, index) => (
<IntegrationBlock
key={index}
title={block.title}
description={block.description}
icon_url={block.icon_url}
/>
))}
</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>
);
};

View File

@@ -1,64 +0,0 @@
import React, { useState, useEffect } from "react";
import Integration from "../Integration";
import { integrationsListData } from "../../testing_data";
import { useBlockMenuContext } from "../block-menu-provider";
export interface IntegrationData {
title: string;
icon_url: string;
description: string;
number_of_blocks: number;
}
const IntegrationList: React.FC = ({}) => {
const { setIntegration } = useBlockMenuContext();
const [integrations, setIntegrations] = useState<IntegrationData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
// TEMPORARY FETCHING
useEffect(() => {
const fetchIntegrations = async () => {
setIsLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
setIntegrations(integrationsListData);
} catch (error) {
console.error("Failed to fetch integrations:", error);
} finally {
setIsLoading(false);
}
};
fetchIntegrations();
}, []);
if (isLoading) {
return (
<div className="space-y-3">
{Array(5)
.fill(null)
.map((_, index) => (
<Integration.Skeleton key={index} />
))}
</div>
);
}
return (
<div className="space-y-3">
{integrations.map((integration, index) => (
<Integration
key={index}
title={integration.title}
icon_url={integration.icon_url}
description={integration.description}
number_of_blocks={integration.number_of_blocks}
onClick={() => setIntegration(integration.title)}
/>
))}
</div>
);
};
export default IntegrationList;

View File

@@ -1,14 +1,19 @@
import React from "react";
import IntegrationList from "./IntegrationList";
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 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200 transition-all duration-200">
<div className="w-full px-4 pb-4">
{integration == "" ? <IntegrationList /> : <IntegrationBlocks />}
<IntegrationBlocks />
</div>
</div>
);

View File

@@ -1,58 +1,66 @@
import React, { useState, useEffect } from "react";
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import { marketplaceAgentData } from "../../testing_data";
export interface MarketplaceAgent {
id: number;
title: string;
image_url: string;
creator_name: string;
number_of_runs: number;
}
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
const MarketplaceAgentsContent: React.FC = () => {
const [agents, setAgents] = useState<MarketplaceAgent[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// TEMPORARY FETCHING
useEffect(() => {
const fetchAgents = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 1500));
setAgents(marketplaceAgentData);
setLoading(false);
} catch (err) {
setLoading(false);
}
};
fetchAgents();
}, []);
const {
data: agents,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "store-agents" },
pageSize: 10,
});
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{Array(5)
.fill(null)
.map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={index} />
))}
{[0, 1, 2, 3, 4].map((index) => (
<MarketplaceAgentBlock.Skeleton key={index} />
))}
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load marketplace agents"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<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.id}
title={agent.title}
image_url={agent.image_url}
creator_name={agent.creator_name}
number_of_runs={agent.number_of_runs}
key={agent.slug}
title={agent.agent_name}
image_url={agent.agent_image}
creator_name={agent.creator}
number_of_runs={agent.runs}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);

View File

@@ -1,58 +1,66 @@
import React, { useState, useEffect } from "react";
import React from "react";
import UGCAgentBlock from "../UGCAgentBlock";
import { myAgentData } from "../../testing_data";
export interface UserAgent {
id: number;
title: string;
edited_time: string;
version: number;
image_url: string;
}
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
const MyAgentsContent: React.FC = () => {
const [agents, setAgents] = useState<UserAgent[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// TEMPORARY FETCHING
useEffect(() => {
const fetchAgents = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 1500));
setAgents(myAgentData);
setLoading(false);
} catch (err) {
setLoading(false);
}
};
fetchAgents();
}, []);
const {
data: agents,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "library-agents" },
pageSize: 10,
});
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{Array(5)
.fill(null)
.map((_, index) => (
<UGCAgentBlock.Skeleton key={index} />
))}
{[0, 1, 2, 3, 4].map((index) => (
<UGCAgentBlock.Skeleton key={index} />
))}
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load library agents"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<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.title}
edited_time={agent.edited_time}
version={agent.version}
title={agent.name}
edited_time={agent.updated_at}
version={agent.graph_version}
image_url={agent.image_url}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<UGCAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);

View File

@@ -1,29 +1,8 @@
import React, { useEffect, useState } from "react";
import BlocksList from "./BlocksList";
import { BlockListType } from "./BlockMenuDefaultContent";
import { outputBlocksListData } from "../../testing_data";
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const OutputBlocksContent: React.FC = () => {
const [blocks, setBlocks] = useState<BlockListType[]>([]);
const [loading, setLoading] = useState(true);
// TEMPORARY FETCHING
useEffect(() => {
const fetchBlocks = async () => {
setLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
setBlocks(outputBlocksListData);
} catch (error) {
console.error("Error fetching blocks:", error);
} finally {
setLoading(false);
}
};
fetchBlocks();
}, []);
return <BlocksList blocks={blocks} loading={loading} />;
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
};
export default OutputBlocksContent;
export default OutputBlocksContent;

View File

@@ -0,0 +1,76 @@
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 (loading) {
return (
<div className="w-full space-y-3 p-4">
{[0, 1, 3].map((categoryIndex) => (
<Fragment key={categoryIndex}>
{categoryIndex > 0 && (
<div className="my-4 h-[1px] w-full bg-zinc-100" />
)}
{[0, 1, 2].map((blockIndex) => (
<Block.Skeleton key={`${categoryIndex}-${blockIndex}`} />
))}
</Fragment>
))}
</div>
);
}
if (error) {
return (
<div className="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,74 @@
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 className="w-full space-y-3 p-4">
{[0, 1, 3].map((integrationIndex) => (
<Integration.Skeleton key={integrationIndex} />
))}
</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

@@ -2,81 +2,86 @@ import React, { useEffect, useState } from "react";
import SearchHistoryChip from "../SearchHistoryChip";
import IntegrationChip from "../IntegrationChip";
import Block from "../Block";
import {
integrationsData,
topBlocksData,
recentSearchesData,
} from "../../testing_data";
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 } =
const { setIntegration, setDefaultState, setSearchQuery, addNode } =
useBlockMenuContext();
const [recentSearches, setRecentSearches] = useState<string[] | null>(null);
const [integrations, setIntegrations] = useState<
{ icon_url: string; name: string }[] | null
>(null);
const [topBlocks, setTopBlocks] = useState<
{ title: string; description: string }[] | null
>(null);
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 = 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);
}
};
// TEMPORARY FETCHING
useEffect(() => {
const fetchData = async () => {
try {
const fetchRecentSearches = async (): Promise<string[]> => {
await new Promise((resolve) => setTimeout(resolve, 300));
return recentSearchesData;
};
fetchSuggestions();
}, [api]);
const fetchIntegrations = async (): Promise<
{ icon_url: string; name: string }[]
> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return integrationsData;
};
const fetchTopBlocks = async (): Promise<
{ title: string; description: string }[]
> => {
await new Promise((resolve) => setTimeout(resolve, 600));
return topBlocksData;
};
const [
recentSearchesDataFetched,
integrationsDataFetched,
topBlocksDataFetched,
] = await Promise.all([
fetchRecentSearches(),
fetchIntegrations(),
fetchTopBlocks(),
]);
setRecentSearches(recentSearchesDataFetched);
setIntegrations(integrationsDataFetched);
setTopBlocks(topBlocksDataFetched);
} catch (error) {
console.error("Error fetching data:", error);
}
};
fetchData();
}, []);
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 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<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="space-y-2.5">
<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="flex flex-nowrap gap-2 overflow-x-auto scrollbar-hide">
{recentSearches
? recentSearches.map((search, index) => (
<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}
@@ -97,19 +102,19 @@ const SuggestionContent: React.FC = () => {
{/* Integrations */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-xs font-medium leading-[1.25rem] text-zinc-500">
<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">
{integrations
? integrations.map((integration, index) => (
{!loading && suggestionsData
? suggestionsData.providers.map((provider, index) => (
<IntegrationChip
key={`integration-${index}`}
icon_url={integration.icon_url}
name={integration.name}
icon_url={`/integrations/${provider}.png`}
name={provider}
onClick={() => {
setDefaultState("integrations");
setIntegration(integration.name);
setIntegration(provider as CredentialsProviderName);
}}
/>
))
@@ -129,12 +134,15 @@ const SuggestionContent: React.FC = () => {
Top blocks
</p>
<div className="space-y-2">
{topBlocks
? topBlocks.map((block, index) => (
{!loading && suggestionsData
? suggestionsData.top_blocks.map((block, index) => (
<Block
key={`block-${index}`}
title={block.title}
title={block.name}
description={block.description}
onClick={() => {
addNode(block);
}}
/>
))
: Array(3)

View File

@@ -1,15 +1,147 @@
import React from "react";
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 } = useBlockMenuContext();
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>(0);
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 {
// Prepare filter array from active categories
const activeCategories = Object.entries(filters.categories)
.filter(([_, isActive]) => isActive)
.map(([category, _]) => category)
.filter((category) => category !== "templates") // API doesn't support templates filter
.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(0);
}
} 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(0);
setHasMore(true);
setError(null);
fetchSearchData(0, false);
} else {
setSearchData([]);
setError(null);
setPage(0);
setHasMore(true);
}
}, [searchQuery, searchId, filters, fetchSearchData, setSearchData]);
return (
<div className="scrollbar-thumb-rounded h-full space-y-4 overflow-y-auto py-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<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 />
<SearchList
isLoading={isLoading}
loadingMore={loadingMore}
hasMore={hasMore}
error={error}
onRetry={() => {
setPage(0);
setError(null);
fetchSearchData(0, false);
}}
/>
</div>
);
};

View File

@@ -69,7 +69,7 @@ export default function FilterSheet({
integrations: false,
marketplace_agents: false,
my_agents: false,
templates: false,
providers: false,
},
createdBy: [],
};

View File

@@ -2,23 +2,20 @@ import { useState, useEffect, useCallback } from "react";
import FilterChip from "../FilterChip";
import FilterSheet from "./FilterSheet";
import { CategoryKey, useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const FiltersList = () => {
const { setCreators, filters, setFilters } = useBlockMenuContext();
const { filters, setFilters, categoryCounts, setCategoryCounts } =
useBlockMenuContext();
const api = useBackendAPI();
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" },
{ key: "templates", name: "Templates" },
{ key: "providers", name: "Providers" },
];
// TEMPORARY FETCHING
useEffect(() => {
const mockCreators = ["Abhi", "Abhi 1", "Abhi 2", "Abhi 3", "Abhi 4"];
setCreators(mockCreators);
}, [setCreators]);
const handleCategoryFilter = (category: CategoryKey) => {
setFilters({
...filters,
@@ -43,6 +40,10 @@ const FiltersList = () => {
[filters, setFilters],
);
useEffect(() => {
console.log(categoryCounts);
}, [categoryCounts]);
return (
<div className="flex flex-nowrap gap-3 overflow-x-auto scrollbar-hide">
<FilterSheet categories={categories} />
@@ -64,7 +65,7 @@ const FiltersList = () => {
Object.values(filters.categories).filter(Boolean).length === 1 &&
filters.categories[category.key]
}
number={103}
number={categoryCounts[category.key]}
selected={filters.categories[category.key]}
onClick={() => handleCategoryFilter(category.key)}
/>

View File

@@ -1,5 +1,4 @@
import { searchingData } from "../../testing_data";
import { useEffect, useState } from "react";
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import Block from "../Block";
import UGCAgentBlock from "../UGCAgentBlock";
@@ -7,27 +6,41 @@ import AiBlock from "./AiBlock";
import IntegrationBlock from "../IntegrationBlock";
import { SearchItem, useBlockMenuContext } from "../block-menu-provider";
import NoSearchResult from "./NoSearchResult";
import { Button } from "@/components/ui/button";
const SearchList = () => {
const { searchQuery, searchData, setSearchData } = useBlockMenuContext();
const [isLoading, setIsLoading] = useState<boolean>(true);
interface SearchListProps {
isLoading: boolean;
loadingMore: boolean;
hasMore: boolean;
error: string | null;
onRetry: () => void;
}
useEffect(() => {
// TEMPORARY FETCHING
const fetchData = async () => {
setIsLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1500));
setSearchData(searchingData as SearchItem[]);
} catch (error) {
console.error("Error fetching search data:", error);
} finally {
setIsLoading(false);
}
};
const SearchList: React.FC<SearchListProps> = ({
isLoading,
loadingMore,
hasMore,
error,
onRetry,
}) => {
const { searchQuery, addNode, searchData } = useBlockMenuContext();
fetchData();
}, [searchQuery, setSearchData]);
// Need to change it once, we got provider blocks
const getBlockType = (item: any) => {
if (item.id && item.name && item.inputSchema && item.outputSchema) {
return "block";
}
if (item.name && typeof item.integration_count === "number") {
return "provider";
}
if (item.id && item.graph_id && item.status) {
return "library_agent";
}
if (item.slug && item.agent_name && item.runs !== undefined) {
return "store_agent";
}
return null;
};
if (isLoading) {
return (
@@ -44,6 +57,26 @@ const SearchList = () => {
);
}
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 />;
}
@@ -53,62 +86,72 @@ const SearchList = () => {
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Search results
</p>
{searchData.map((item: SearchItem, index: number) => {
switch (item.type) {
case "marketing_agent":
{searchData.map((item: any, index: number) => {
const blockType = getBlockType(item);
switch (blockType) {
case "store_agent":
return (
<MarketplaceAgentBlock
key={index}
highlightedText={searchQuery}
title={item.title}
image_url={item.image_url}
creator_name={item.creator_name}
number_of_runs={item.number_of_runs}
title={item.agent_name}
image_url={item.agent_image}
creator_name={item.creator}
number_of_runs={item.runs}
/>
);
case "block":
return (
<Block
key={index}
title={item.title}
title={item.name}
highlightedText={searchQuery}
description={item.description}
onClick={() => {
addNode(item);
}}
/>
);
case "integration_block":
case "provider":
// Here we do need the Integration blocks list, not integration itself
return (
<IntegrationBlock
key={index}
title={item.title}
title={item.name}
highlightedText={searchQuery}
icon_url={`/integrations/${item.name}.png`}
description={item.description}
icon_url={item.icon_url}
onClick={() => {
addNode(item);
}}
/>
);
case "my_agent":
case "library_agent":
return (
<UGCAgentBlock
key={index}
title={item.title}
title={item.name}
highlightedText={searchQuery}
image_url={item.image_url}
version={item.version}
edited_time={item.edited_time}
/>
);
case "ai":
return (
<AiBlock
key={index}
title={item.title}
description={item.description}
ai_name={item.ai_name}
version={item.graph_version}
edited_time={item.updated_at}
/>
);
// currently our backend does not support ai blocks
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>
);
};

View File

@@ -1,541 +0,0 @@
// Default state data
import { SearchItem } from "./block-menu/block-menu-provider";
import { BlockCategory } from "./block-menu/default/AllBlocksContent";
import { BlockListType } from "./block-menu/default/BlockMenuDefaultContent";
import { IntegrationBlockData } from "./block-menu/default/IntegrationBlocks";
import { IntegrationData } from "./block-menu/default/IntegrationList";
import { MarketplaceAgent } from "./block-menu/default/MarketplaceAgentsContent";
import { UserAgent } from "./block-menu/default/MyAgentsContent";
// Suggestion
export const recentSearchesData = [
"image generator",
"deepfake",
"competitor analysis",
"market research",
"AI tools",
"content creation",
"data visualization",
"automation workflow",
"analytics dashboard",
];
// Define data for integrations
export const integrationsData = [
{
icon_url: "/integrations/x.png",
name: "Twitter",
},
{ icon_url: "/integrations/github.png", name: "Github" },
{ icon_url: "/integrations/hubspot.png", name: "Hubspot" },
{ icon_url: "/integrations/discord.png", name: "Discord" },
{ icon_url: "/integrations/medium.png", name: "Medium" },
{ icon_url: "/integrations/todoist.png", name: "Todoist" },
];
// Define data for top blocks
export const topBlocksData = [
{
title: "Find in Dictionary",
description: "Enables your agent to chat with users in natural language.",
},
{
title: "Web Search",
description: "Allows your agent to search the web for information.",
},
{
title: "Code Interpreter",
description: "Helps your agent understand and execute code snippets.",
},
{
title: "Data Analysis",
description:
"Enables your agent to analyze data and create visualizations.",
},
{
title: "File Manager",
description: "Gives your agent the ability to manage files and documents.",
},
];
// All Blocks
export const allBlocksDataWithCategories: BlockCategory[] = [
{
name: "AI",
count: 10,
items: [
{
title: "Natural Language Processing",
description:
"Enables your agent to chat with users in natural language.",
},
{
title: "Sentiment Analysis",
description:
"Analyzes the sentiment of user messages to respond appropriately.",
},
{
title: "Text Generation",
description:
"Creates human-like text based on the context and inputs provided.",
},
{
title: "Entity Recognition",
description: "Identifies and extracts entities from user messages.",
},
],
},
{
name: "Basic",
count: 6,
items: [
{
title: "Condition",
description: "Creates branching logic based on specific conditions.",
},
{
title: "Loop",
description: "Repeats actions until a specific condition is met.",
},
{
title: "Variable",
description:
"Stores and manages data for use throughout your workflow.",
},
],
},
{
name: "Communication",
count: 6,
items: [
{
title: "Email Sender",
description: "Sends emails to users based on triggers or conditions.",
},
{
title: "SMS Notification",
description:
"Sends text message notifications to users' mobile devices.",
},
{
title: "Webhook",
description:
"Integrates with external services through HTTP callbacks.",
},
],
},
];
export const actionBlocksListData: BlockListType[] = [
{
id: 1,
title: "Date Input Block",
description: "Input a date into your agent.",
},
{
id: 2,
title: "Dropdown input",
description: "Give your users the ability to select from a dropdown menu",
},
{
id: 3,
title: "File upload",
description: "Upload a file to your agent",
},
{
id: 4,
title: "Text input",
description: "Allow users to select multiple options using checkboxes",
},
];
export const inputBlocksListData: BlockListType[] = [
{
id: 1,
title: "Text Field",
description: "Collect single line text input from users.",
},
{
id: 2,
title: "Checkbox",
description: "Allow users to select multiple options using checkboxes.",
},
{
id: 3,
title: "Radio Button",
description: "Let users choose one option from a list of alternatives.",
},
{
id: 4,
title: "Textarea",
description: "Collect multi-line text input from users.",
},
{
id: 5,
title: "Number Input",
description: "Collect numerical values with optional min/max constraints.",
},
];
export const outputBlocksListData: BlockListType[] = [
{
id: 1,
title: "Display Text",
description: "Show formatted text content to users.",
},
{
id: 2,
title: "Image Output",
description: "Display images, charts, or visual content.",
},
{
id: 3,
title: "Table Display",
description: "Present data in an organized tabular format.",
},
{
id: 4,
title: "PDF Generation",
description: "Create and export data as PDF documents.",
},
{
id: 5,
title: "Status Alert",
description: "Show success, error, or informational alerts to users.",
},
];
export const integrationsListData: IntegrationData[] = [
{
title: "Twitter",
icon_url: "/integrations/x.png",
description:
"All twitter blocks, It has everthing to interact with twitter",
number_of_blocks: 4,
},
{
title: "Discord",
icon_url: "/integrations/discord.png",
description:
"All Discord blocks, It has everthing to interact with discord",
number_of_blocks: 4,
},
{
title: "Github",
icon_url: "/integrations/github.png",
description: "All Github blocks, It has everthing to interact with github",
number_of_blocks: 4,
},
{
title: "Hubspot",
icon_url: "/integrations/hubspot.png",
description:
"All Hubspot blocks, It has everthing to interact with Hubspot",
number_of_blocks: 2,
},
{
title: "Medium",
icon_url: "/integrations/medium.png",
description: "All Medium blocks, It has everything to interact with Medium",
number_of_blocks: 4,
},
{
title: "Todoist",
icon_url: "/integrations/todoist.png",
description:
"All Todoist blocks, It has everything to interact with Todoist",
number_of_blocks: 4,
},
];
export const marketplaceAgentData: MarketplaceAgent[] = [
{
id: 1,
title: "turtle test",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 1000,
},
{
id: 2,
title: "turtle test 1",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 1324,
},
{
id: 3,
title: "turtle test 2",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 10030,
},
{
id: 4,
title: "turtle test 3",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 324,
},
{
id: 5,
title: "turtle test",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 4345,
},
{
id: 6,
title: "turtle test",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 324,
},
{
id: 7,
title: "turtle test 3",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 324,
},
{
id: 8,
title: "turtle test",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 4345,
},
{
id: 9,
title: "turtle test",
image_url: "/placeholder.png",
creator_name: "Autogpt",
number_of_runs: 324,
},
];
export const myAgentData: UserAgent[] = [
{
id: 1,
title: "My Agent 1",
edited_time: "23rd April",
version: 3,
image_url: "/placeholder.png",
},
{
id: 2,
title: "My Agent 2",
edited_time: "21st April",
version: 4,
image_url: "/placeholder.png",
},
{
id: 3,
title: "My Agent 3",
edited_time: "23rd May",
version: 7,
image_url: "/placeholder.png",
},
{
id: 4,
title: "My Agent 4",
edited_time: "23rd April",
version: 3,
image_url: "/placeholder.png",
},
{
id: 5,
title: "My Agent 5",
edited_time: "23rd April",
version: 3,
image_url: "/placeholder.png",
},
{
id: 6,
title: "My Agent 6",
edited_time: "23rd April",
version: 3,
image_url: "/placeholder.png",
},
];
export const integrationBlocksData: Record<string, IntegrationBlockData[]> = {
Twitter: [
{
title: "Twitter Blocks: Post tweet",
description: "Post tweet on twitter",
icon_url: "/integrations/x.png",
},
{
title: "Twitter Blocks: Delete tweet",
description: "Delete tweet on twitter",
icon_url: "/integrations/x.png",
},
{
title: "Twitter Blocks: Update tweet",
description: "Update tweet on twitter",
icon_url: "/integrations/x.png",
},
{
title: "Twitter Blocks: Retweet tweet",
description: "Retweet tweet on twitter",
icon_url: "/integrations/x.png",
},
],
Discord: [
{
title: "Discord Blocks: Create",
description: "Create message on discord",
icon_url: "/integrations/discord.png",
},
{
title: "Discord Blocks: Delete",
description: "Delete message on discord",
icon_url: "/integrations/discord.png",
},
{
title: "Discord Blocks: Update",
description: "Update message on discord",
icon_url: "/integrations/discord.png",
},
{
title: "Discord Blocks: Read",
description: "Read message on discord",
icon_url: "/integrations/discord.png",
},
],
Github: [
{
title: "Github Blocks: Create PR",
description: "Create pull request on github",
icon_url: "/integrations/github.png",
},
{
title: "Github Blocks: Merge PR",
description: "Merge pull request on github",
icon_url: "/integrations/github.png",
},
],
Hubspot: [
{
title: "Hubspot Blocks: Create Contact",
description: "Create contact on hubspot",
icon_url: "/integrations/hubspot.png",
},
{
title: "Hubspot Blocks: Update Contact",
description: "Update contact on hubspot",
icon_url: "/integrations/hubspot.png",
},
],
Medium: [
{
title: "Medium Blocks: Post Article",
description: "Post article on medium",
icon_url: "/integrations/medium.png",
},
{
title: "Medium Blocks: Delete Article",
description: "Delete article on medium",
icon_url: "/integrations/medium.png",
},
],
Todoist: [
{
title: "Todoist Blocks: Create Task",
description: "Create task on todoist",
icon_url: "/integrations/todoist.png",
},
{
title: "Todoist Blocks: Complete Task",
description: "Complete task on todoist",
icon_url: "/integrations/todoist.png",
},
],
};
export const searchingData: SearchItem[] = [
{
type: "marketing_agent" as const,
title: marketplaceAgentData[0].title,
image_url: marketplaceAgentData[0].image_url,
creator_name: marketplaceAgentData[0].creator_name,
number_of_runs: marketplaceAgentData[0].number_of_runs,
},
{
type: "ai",
title: "Natural Language Processing",
description: "Enables your agent to chat with users in natural language.",
ai_name: "Claude 3.5 Sonnet",
},
{
type: "integration_block" as const,
title: integrationsListData[0].title,
description: integrationsListData[0].description,
icon_url: integrationsListData[0].icon_url,
number_of_blocks: integrationsListData[0].number_of_blocks,
},
{
type: "marketing_agent" as const,
title: marketplaceAgentData[1].title,
image_url: marketplaceAgentData[1].image_url,
creator_name: marketplaceAgentData[1].creator_name,
number_of_runs: marketplaceAgentData[1].number_of_runs,
},
{
type: "block" as const,
title: topBlocksData[0].title,
description: topBlocksData[0].description,
},
{
type: "my_agent" as const,
title: myAgentData[0].title,
image_url: myAgentData[0].image_url,
edited_time: myAgentData[0].edited_time,
version: myAgentData[0].version,
},
{
type: "ai",
title: "Sentiment Analysis",
description:
"Analyzes the sentiment of user messages to respond appropriately.",
ai_name: "Claude 3.5 Sonnet",
},
{
type: "block" as const,
title: topBlocksData[1].title,
description: topBlocksData[1].description,
},
{
type: "marketing_agent" as const,
title: marketplaceAgentData[2].title,
image_url: marketplaceAgentData[2].image_url,
creator_name: marketplaceAgentData[2].creator_name,
number_of_runs: marketplaceAgentData[2].number_of_runs,
},
{
type: "integration_block" as const,
title: integrationsListData[1].title,
description: integrationsListData[1].description,
icon_url: integrationsListData[1].icon_url,
number_of_blocks: integrationsListData[1].number_of_blocks,
},
{
type: "my_agent" as const,
title: myAgentData[1].title,
image_url: myAgentData[1].image_url,
edited_time: myAgentData[1].edited_time,
version: myAgentData[1].version,
},
];

View File

@@ -0,0 +1 @@
export { usePagination } from './usePagination';

View File

@@ -0,0 +1,232 @@
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Block,
BlockRequest,
Provider,
StoreAgent,
LibraryAgent,
LibraryAgentSortEnum,
} from "@/lib/autogpt-server-api";
type BlocksPaginationRequest = { apiType: "blocks" } & BlockRequest;
type ProvidersPaginationRequest = { apiType: "providers" } & {
page?: number;
page_size?: number;
};
type StoreAgentsPaginationRequest = { apiType: "store-agents" } & {
featured?: boolean;
creator?: string;
sorted_by?: string;
search_query?: string;
category?: string;
page?: number;
page_size?: number;
};
type LibraryAgentsPaginationRequest = { apiType: "library-agents" } & {
search_term?: string;
sort_by?: LibraryAgentSortEnum;
page?: number;
page_size?: number;
};
type PaginationRequest =
| BlocksPaginationRequest
| ProvidersPaginationRequest
| StoreAgentsPaginationRequest
| LibraryAgentsPaginationRequest;
interface UsePaginationOptions<T extends PaginationRequest> {
request: T;
pageSize?: number;
enabled?: boolean;
}
interface UsePaginationReturn<T> {
data: T[];
loading: boolean;
loadingMore: boolean;
hasMore: boolean;
error: string | null;
scrollRef: React.RefObject<HTMLDivElement>;
refresh: () => void;
loadMore: () => void;
}
type GetReturnType<T> = T extends BlocksPaginationRequest
? Block
: T extends ProvidersPaginationRequest
? Provider
: T extends StoreAgentsPaginationRequest
? StoreAgent
: T extends LibraryAgentsPaginationRequest
? LibraryAgent
: never;
export const usePagination = <T extends PaginationRequest>({
request,
pageSize = 10,
enabled = true, // to allow pagination or not
}: UsePaginationOptions<T>): UsePaginationReturn<GetReturnType<T>> => {
const [data, setData] = useState<GetReturnType<T>[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [error, setError] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const isLoadingRef = useRef(false);
const requestRef = useRef(request);
const api = useBackendAPI();
// because we are using this pagination for multiple components
requestRef.current = request;
const fetchData = useCallback(
async (page: number, isLoadMore = false) => {
if (isLoadingRef.current || !enabled) return;
isLoadingRef.current = true;
if (isLoadMore) {
setLoadingMore(true);
} else {
setLoading(true);
}
setError(null);
try {
let response;
let newData: GetReturnType<T>[];
let pagination;
const currentRequest = requestRef.current;
const requestWithPagination = {
...currentRequest,
page,
page_size: pageSize,
};
switch (currentRequest.apiType) {
case "blocks":
const { apiType: _, ...blockRequest } = requestWithPagination;
response = await api.getBuilderBlocks(blockRequest);
newData = response.blocks as GetReturnType<T>[];
pagination = response.pagination;
break;
case "providers":
const { apiType: __, ...providerRequest } = requestWithPagination;
response = await api.getProviders(providerRequest);
newData = response.providers as GetReturnType<T>[];
pagination = response.pagination;
break;
case "store-agents":
const { apiType: ___, ...storeAgentRequest } =
requestWithPagination;
response = await api.getStoreAgents(storeAgentRequest);
newData = response.agents as GetReturnType<T>[];
pagination = response.pagination;
break;
case "library-agents":
const { apiType: ____, ...libraryAgentRequest } =
requestWithPagination;
response = await api.listLibraryAgents(libraryAgentRequest);
newData = response.agents as GetReturnType<T>[];
pagination = response.pagination;
break;
default:
throw new Error(
`Unknown request type: ${(currentRequest as any).apiType}`,
);
}
if (isLoadMore) {
setData((prev) => [...prev, ...newData]);
} else {
setData(newData);
}
setHasMore(page < pagination.total_pages);
setCurrentPage(page);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch data";
setError(errorMessage);
console.error("Error fetching data:", err);
} finally {
setLoading(false);
setLoadingMore(false);
isLoadingRef.current = false;
}
},
[api, pageSize, enabled],
);
const handleScroll = useCallback(() => {
const scrollElement = scrollRef.current;
if (
!scrollElement ||
loadingMore ||
!hasMore ||
isLoadingRef.current ||
!enabled
)
return;
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
const threshold = 100;
if (scrollTop + clientHeight >= scrollHeight - threshold) {
fetchData(currentPage + 1, true);
}
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
const refresh = useCallback(() => {
setCurrentPage(1);
setHasMore(true);
setError(null);
fetchData(1);
}, [fetchData]);
const loadMore = useCallback(() => {
if (!loadingMore && hasMore && !isLoadingRef.current && enabled) {
fetchData(currentPage + 1, true);
}
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
const requestString = JSON.stringify(request);
useEffect(() => {
if (enabled) {
setCurrentPage(1);
setHasMore(true);
setError(null);
setData([]);
fetchData(1);
}
}, [requestString, enabled, fetchData]);
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement && enabled) {
scrollElement.addEventListener("scroll", handleScroll);
return () => scrollElement.removeEventListener("scroll", handleScroll);
}
}, [handleScroll, enabled]);
return {
data,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
loadMore,
};
};

View File

@@ -10310,6 +10310,11 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
get-nonce "^1.0.0"
tslib "^2.0.0"
react-timeago@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/react-timeago/-/react-timeago-8.2.0.tgz#a6c9c81f1ed8565f87d297d012c467944a0176be"
integrity sha512-RWDlG3Jj+iwv+yNEDweA/Qk1mxE8i/Oc4oW8Irp29ZfBp+eNpqqYPMLPYQJyfRMJcGB8CmWkEGMYhB4fW8eZlQ==
react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"