Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update
@@ -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",
|
||||
|
||||
BIN
autogpt_platform/frontend/public/integrations/anthropic.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
autogpt_platform/frontend/public/integrations/apollo.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
autogpt_platform/frontend/public/integrations/d_id.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/e2b.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
autogpt_platform/frontend/public/integrations/exa.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
autogpt_platform/frontend/public/integrations/fal.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
autogpt_platform/frontend/public/integrations/google_maps.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
autogpt_platform/frontend/public/integrations/groq.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
autogpt_platform/frontend/public/integrations/ideogram.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
autogpt_platform/frontend/public/integrations/jina.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
autogpt_platform/frontend/public/integrations/llama_api.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
autogpt_platform/frontend/public/integrations/nvidia.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
autogpt_platform/frontend/public/integrations/ollama.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
autogpt_platform/frontend/public/integrations/open_router.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
autogpt_platform/frontend/public/integrations/openai.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
autogpt_platform/frontend/public/integrations/replicate.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
autogpt_platform/frontend/public/integrations/revid.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
autogpt_platform/frontend/public/integrations/screenshotone.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
autogpt_platform/frontend/public/integrations/slant3d.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
autogpt_platform/frontend/public/integrations/smartlead.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
autogpt_platform/frontend/public/integrations/twitter.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
BIN
autogpt_platform/frontend/public/integrations/zerobounce.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -43,7 +43,7 @@ const BlockMenuSidebar: React.FC = ({}) => {
|
||||
type: "integrations",
|
||||
number: 24,
|
||||
onClick: () => {
|
||||
setIntegration("");
|
||||
setIntegration(null);
|
||||
setDefaultState("integrations");
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function FilterSheet({
|
||||
integrations: false,
|
||||
marketplace_agents: false,
|
||||
my_agents: false,
|
||||
templates: false,
|
||||
providers: false,
|
||||
},
|
||||
createdBy: [],
|
||||
};
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
1
autogpt_platform/frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { usePagination } from './usePagination';
|
||||
232
autogpt_platform/frontend/src/hooks/usePagination.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
|
||||