mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-09 22:35:54 -05:00
Compare commits
99 Commits
fix/vector
...
abhi/block
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7926919e3e | ||
|
|
27e53aa3dd | ||
|
|
a24673d15f | ||
|
|
9d2d9606e8 | ||
|
|
91407dfc33 | ||
|
|
851919d2d5 | ||
|
|
d6acb02cb6 | ||
|
|
9c07206725 | ||
|
|
4bd3447301 | ||
|
|
8adc9f967d | ||
|
|
349b70c4bc | ||
|
|
9ecfa1e1f1 | ||
|
|
4e17f9c49e | ||
|
|
31fdeeb706 | ||
|
|
e42b24c029 | ||
|
|
2d52a57a21 | ||
|
|
f45123f6b6 | ||
|
|
d524518f41 | ||
|
|
81d1b28d92 | ||
|
|
4e4e754ac1 | ||
|
|
e409d7aa34 | ||
|
|
8312a339c2 | ||
|
|
5b45d246ef | ||
|
|
5c7c7ca874 | ||
|
|
c93c5e35ba | ||
|
|
ce989b1bf7 | ||
|
|
c1c919b88b | ||
|
|
21a91fe9fd | ||
|
|
b2f3d8c1f2 | ||
|
|
46ab2e3b20 | ||
|
|
5b40700299 | ||
|
|
1a97020eeb | ||
|
|
39d03f2090 | ||
|
|
8088d294f4 | ||
|
|
31266949ed | ||
|
|
f4eb00a6ad | ||
|
|
f75cc0dd11 | ||
|
|
21b612625f | ||
|
|
eec0d276d5 | ||
|
|
c6941e7f6e | ||
|
|
325684a10f | ||
|
|
cf057cbbda | ||
|
|
f3a7be1fd3 | ||
|
|
97bcb0f95e | ||
|
|
dd71d65706 | ||
|
|
2b2d26bcde | ||
|
|
67f6f43e1b | ||
|
|
a3409c9578 | ||
|
|
7f82457ea4 | ||
|
|
a5c0fabc00 | ||
|
|
09dba93a4a | ||
|
|
ea2cd3e7bf | ||
|
|
d3d0ccf732 | ||
|
|
d8d5d6ec0c | ||
|
|
f45b09c0b5 | ||
|
|
1e89b6d3a4 | ||
|
|
950a85e179 | ||
|
|
c5e3148145 | ||
|
|
a135ba3f0b | ||
|
|
fe95e27226 | ||
|
|
711ca10cc9 | ||
|
|
1346d8230c | ||
|
|
07c84a4757 | ||
|
|
596824c1e7 | ||
|
|
79afa6db99 | ||
|
|
e034c16f31 | ||
|
|
9012eff1ac | ||
|
|
0361ea4aa4 | ||
|
|
6f1c522ea3 | ||
|
|
2d654bf64b | ||
|
|
bb69e32fee | ||
|
|
1be830835b | ||
|
|
a2a4d546f7 | ||
|
|
3053a7bd06 | ||
|
|
bbf4108136 | ||
|
|
95387bcf78 | ||
|
|
e1fc56e6f3 | ||
|
|
2a06956802 | ||
|
|
32231ff80f | ||
|
|
d0b23c085f | ||
|
|
e718d3d3d8 | ||
|
|
1971a62684 | ||
|
|
e125b5923c | ||
|
|
c6942e4e6f | ||
|
|
c9e421a219 | ||
|
|
7868373897 | ||
|
|
f1c8399e0e | ||
|
|
97ba69ef1c | ||
|
|
773e1488bf | ||
|
|
4273be59ba | ||
|
|
06e524788a | ||
|
|
bc08012771 | ||
|
|
4af0aedebd | ||
|
|
d22464a75e | ||
|
|
82e3a485f0 | ||
|
|
8165ad5879 | ||
|
|
451284de76 | ||
|
|
1d8c7c5e1a | ||
|
|
34be6a3379 |
@@ -28,6 +28,7 @@ import "@xyflow/react/dist/style.css";
|
|||||||
import { CustomNode } from "./CustomNode";
|
import { CustomNode } from "./CustomNode";
|
||||||
import "./flow.css";
|
import "./flow.css";
|
||||||
import {
|
import {
|
||||||
|
Block,
|
||||||
BlockUIType,
|
BlockUIType,
|
||||||
formatEdgeID,
|
formatEdgeID,
|
||||||
GraphExecutionID,
|
GraphExecutionID,
|
||||||
@@ -53,6 +54,7 @@ import OttoChatWidget from "@/components/OttoChatWidget";
|
|||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||||
import { CronScheduler } from "./cronScheduler";
|
import { CronScheduler } from "./cronScheduler";
|
||||||
|
import { BlockMenu } from "./builder/block-menu/BlockMenu";
|
||||||
|
|
||||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||||
@@ -103,7 +105,6 @@ const FlowEditor: React.FC<{
|
|||||||
setAgentDescription,
|
setAgentDescription,
|
||||||
savedAgent,
|
savedAgent,
|
||||||
availableNodes,
|
availableNodes,
|
||||||
availableFlows,
|
|
||||||
getOutputType,
|
getOutputType,
|
||||||
requestSave,
|
requestSave,
|
||||||
requestSaveAndRun,
|
requestSaveAndRun,
|
||||||
@@ -138,6 +139,10 @@ const FlowEditor: React.FC<{
|
|||||||
// State to control if save popover should be pinned open
|
// State to control if save popover should be pinned open
|
||||||
const [pinSavePopover, setPinSavePopover] = useState(false);
|
const [pinSavePopover, setPinSavePopover] = useState(false);
|
||||||
|
|
||||||
|
const [blockMenuSelected, setBlockMenuSelected] = useState<
|
||||||
|
"save" | "block" | ""
|
||||||
|
>("");
|
||||||
|
|
||||||
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
|
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
|
||||||
|
|
||||||
const [openCron, setOpenCron] = useState(false);
|
const [openCron, setOpenCron] = useState(false);
|
||||||
@@ -471,13 +476,7 @@ const FlowEditor: React.FC<{
|
|||||||
}, [nodes, setViewport, x, y]);
|
}, [nodes, setViewport, x, y]);
|
||||||
|
|
||||||
const addNode = useCallback(
|
const addNode = useCallback(
|
||||||
(blockId: string, nodeType: string, hardcodedValues: any = {}) => {
|
(block: Block) => {
|
||||||
const nodeSchema = availableNodes.find((node) => node.id === blockId);
|
|
||||||
if (!nodeSchema) {
|
|
||||||
console.error(`Schema not found for block ID: ${blockId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Calculate a position to the right of the newly added block, allowing for some margin.
|
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.
|
If adding to the right side causes the new block to collide with an existing block, attempt to place it at the bottom or left.
|
||||||
@@ -494,7 +493,7 @@ const FlowEditor: React.FC<{
|
|||||||
? // we will get all the dimension of nodes, then store
|
? // we will get all the dimension of nodes, then store
|
||||||
findNewlyAddedBlockCoordinates(
|
findNewlyAddedBlockCoordinates(
|
||||||
nodeDimensions,
|
nodeDimensions,
|
||||||
nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500,
|
block.uiType == BlockUIType.NOTE ? 300 : 500,
|
||||||
60,
|
60,
|
||||||
1.0,
|
1.0,
|
||||||
)
|
)
|
||||||
@@ -509,19 +508,19 @@ const FlowEditor: React.FC<{
|
|||||||
type: "custom",
|
type: "custom",
|
||||||
position: viewportCoordinates, // Set the position to the calculated viewport center
|
position: viewportCoordinates, // Set the position to the calculated viewport center
|
||||||
data: {
|
data: {
|
||||||
blockType: nodeType,
|
blockType: block.name,
|
||||||
blockCosts: nodeSchema.costs,
|
blockCosts: block.costs,
|
||||||
title: `${nodeType} ${nodeId}`,
|
title: `${block.name} ${nodeId}`,
|
||||||
description: nodeSchema.description,
|
description: block.description,
|
||||||
categories: nodeSchema.categories,
|
categories: block.categories,
|
||||||
inputSchema: nodeSchema.inputSchema,
|
inputSchema: block.inputSchema,
|
||||||
outputSchema: nodeSchema.outputSchema,
|
outputSchema: block.outputSchema,
|
||||||
hardcodedValues: hardcodedValues,
|
hardcodedValues: block.hardcodedValues || {},
|
||||||
connections: [],
|
connections: [],
|
||||||
isOutputOpen: false,
|
isOutputOpen: false,
|
||||||
block_id: blockId,
|
block_id: block.id,
|
||||||
isOutputStatic: nodeSchema.staticOutput,
|
isOutputStatic: block.staticOutput,
|
||||||
uiType: nodeSchema.uiType,
|
uiType: block.uiType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -550,7 +549,6 @@ const FlowEditor: React.FC<{
|
|||||||
[
|
[
|
||||||
nodeId,
|
nodeId,
|
||||||
setViewport,
|
setViewport,
|
||||||
availableNodes,
|
|
||||||
addNodes,
|
addNodes,
|
||||||
nodeDimensions,
|
nodeDimensions,
|
||||||
deleteElements,
|
deleteElements,
|
||||||
@@ -632,12 +630,12 @@ const FlowEditor: React.FC<{
|
|||||||
const editorControls: Control[] = [
|
const editorControls: Control[] = [
|
||||||
{
|
{
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
icon: <IconUndo2 />,
|
icon: <IconUndo2 className="h-5 w-5" strokeWidth={2} />,
|
||||||
onClick: handleUndo,
|
onClick: handleUndo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Redo",
|
label: "Redo",
|
||||||
icon: <IconRedo2 />,
|
icon: <IconRedo2 className="h-5 w-5" strokeWidth={2} />,
|
||||||
onClick: handleRedo,
|
onClick: handleRedo,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -685,15 +683,13 @@ const FlowEditor: React.FC<{
|
|||||||
<Controls />
|
<Controls />
|
||||||
<Background className="dark:bg-slate-800" />
|
<Background className="dark:bg-slate-800" />
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
className="absolute z-20"
|
|
||||||
controls={editorControls}
|
controls={editorControls}
|
||||||
topChildren={
|
topChildren={
|
||||||
<BlocksControl
|
<BlockMenu
|
||||||
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
|
pinBlocksPopover={pinBlocksPopover}
|
||||||
blocks={availableNodes}
|
addNode={addNode}
|
||||||
addBlock={addNode}
|
blockMenuSelected={blockMenuSelected}
|
||||||
flows={availableFlows}
|
setBlockMenuSelected={setBlockMenuSelected}
|
||||||
nodes={nodes}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
botChildren={
|
botChildren={
|
||||||
@@ -706,6 +702,8 @@ const FlowEditor: React.FC<{
|
|||||||
agentName={agentName}
|
agentName={agentName}
|
||||||
onNameChange={setAgentName}
|
onNameChange={setAgentName}
|
||||||
pinSavePopover={pinSavePopover}
|
pinSavePopover={pinSavePopover}
|
||||||
|
blockMenuSelected={blockMenuSelected}
|
||||||
|
setBlockMenuSelected={setBlockMenuSelected}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
></ControlPanel>
|
></ControlPanel>
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Block,
|
||||||
|
CredentialsProviderName,
|
||||||
|
LibraryAgent,
|
||||||
|
Provider,
|
||||||
|
StoreAgent,
|
||||||
|
} from "@/lib/autogpt-server-api";
|
||||||
|
import { createContext, ReactNode, useContext, useState } from "react";
|
||||||
|
import { convertLibraryAgentIntoBlock } from "@/lib/utils";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
|
||||||
|
export type SearchItem = Block | Provider | LibraryAgent | StoreAgent;
|
||||||
|
|
||||||
|
export type DefaultStateType =
|
||||||
|
| "suggestion"
|
||||||
|
| "all_blocks"
|
||||||
|
| "input_blocks"
|
||||||
|
| "action_blocks"
|
||||||
|
| "output_blocks"
|
||||||
|
| "integrations"
|
||||||
|
| "marketplace_agents"
|
||||||
|
| "my_agents";
|
||||||
|
|
||||||
|
export type CategoryKey =
|
||||||
|
| "blocks"
|
||||||
|
| "integrations"
|
||||||
|
| "marketplace_agents"
|
||||||
|
| "my_agents";
|
||||||
|
|
||||||
|
export interface Filters {
|
||||||
|
categories: {
|
||||||
|
blocks: boolean;
|
||||||
|
integrations: boolean;
|
||||||
|
marketplace_agents: boolean;
|
||||||
|
my_agents: boolean;
|
||||||
|
providers: boolean;
|
||||||
|
};
|
||||||
|
createdBy: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryCounts = Record<CategoryKey, number>;
|
||||||
|
|
||||||
|
interface BlockMenuContextType {
|
||||||
|
defaultState: DefaultStateType;
|
||||||
|
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
|
||||||
|
integration: CredentialsProviderName | null;
|
||||||
|
setIntegration: React.Dispatch<
|
||||||
|
React.SetStateAction<CredentialsProviderName | null>
|
||||||
|
>;
|
||||||
|
searchQuery: string;
|
||||||
|
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
searchId: string | undefined;
|
||||||
|
setSearchId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
filters: Filters;
|
||||||
|
setFilters: React.Dispatch<React.SetStateAction<Filters>>;
|
||||||
|
searchData: SearchItem[];
|
||||||
|
setSearchData: React.Dispatch<React.SetStateAction<SearchItem[]>>;
|
||||||
|
categoryCounts: CategoryCounts;
|
||||||
|
setCategoryCounts: React.Dispatch<React.SetStateAction<CategoryCounts>>;
|
||||||
|
addNode: (block: Block) => void;
|
||||||
|
handleAddStoreAgent: ({
|
||||||
|
creator_name,
|
||||||
|
slug,
|
||||||
|
}: {
|
||||||
|
creator_name: string;
|
||||||
|
slug: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
loadingSlug: string | null;
|
||||||
|
setLoadingSlug: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockMenuContext = createContext<BlockMenuContextType>(
|
||||||
|
{} as BlockMenuContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface BlockMenuStateProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
addNode: (block: Block) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlockMenuStateProvider({
|
||||||
|
children,
|
||||||
|
addNode,
|
||||||
|
}: BlockMenuStateProviderProps) {
|
||||||
|
const [defaultState, setDefaultState] =
|
||||||
|
useState<DefaultStateType>("suggestion");
|
||||||
|
const [integration, setIntegration] =
|
||||||
|
useState<CredentialsProviderName | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [filters, setFilters] = useState<Filters>({
|
||||||
|
categories: {
|
||||||
|
blocks: false,
|
||||||
|
integrations: false,
|
||||||
|
marketplace_agents: false,
|
||||||
|
my_agents: false,
|
||||||
|
providers: false,
|
||||||
|
},
|
||||||
|
createdBy: [],
|
||||||
|
});
|
||||||
|
const [searchData, setSearchData] = useState<SearchItem[]>([]);
|
||||||
|
|
||||||
|
const [searchId, setSearchId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const [categoryCounts, setCategoryCounts] = useState<CategoryCounts>({
|
||||||
|
blocks: 0,
|
||||||
|
integrations: 0,
|
||||||
|
marketplace_agents: 0,
|
||||||
|
my_agents: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loadingSlug, setLoadingSlug] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
const handleAddStoreAgent = async ({
|
||||||
|
creator_name,
|
||||||
|
slug,
|
||||||
|
}: {
|
||||||
|
creator_name: string;
|
||||||
|
slug: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
setLoadingSlug(slug);
|
||||||
|
const details = await api.getStoreAgent(creator_name, slug);
|
||||||
|
|
||||||
|
if (!details.active_version_id) {
|
||||||
|
console.error(
|
||||||
|
"Cannot add store agent to library: active version ID is missing or undefined",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||||
|
details.active_version_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const block = convertLibraryAgentIntoBlock(libraryAgent);
|
||||||
|
addNode(block);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add store agent:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingSlug(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockMenuContext.Provider
|
||||||
|
value={{
|
||||||
|
defaultState,
|
||||||
|
setDefaultState,
|
||||||
|
integration,
|
||||||
|
setIntegration,
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
searchId,
|
||||||
|
setSearchId,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
searchData,
|
||||||
|
setSearchData,
|
||||||
|
categoryCounts,
|
||||||
|
setCategoryCounts,
|
||||||
|
addNode,
|
||||||
|
handleAddStoreAgent,
|
||||||
|
loadingSlug,
|
||||||
|
setLoadingSlug,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BlockMenuContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBlockMenuContext(): BlockMenuContextType {
|
||||||
|
const context = useContext(BlockMenuContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Error in context of Block");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||||
|
|
||||||
|
const ActionBlocksContent: React.FC = () => {
|
||||||
|
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionBlocksContent;
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState, useEffect, Fragment, useCallback } from "react";
|
||||||
|
import Block from "../Block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { BlockCategoryResponse } from "@/lib/autogpt-server-api";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
import { beautifyString } from "@/lib/utils";
|
||||||
|
|
||||||
|
const AllBlocksContent: React.FC = () => {
|
||||||
|
const { addNode } = useBlockMenuContext();
|
||||||
|
const [categories, setCategories] = useState<BlockCategoryResponse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loadingCategories, setLoadingCategories] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
const fetchBlocks = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await api.getBlockCategories();
|
||||||
|
setCategories(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch block categories:", err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load block categories",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlocks();
|
||||||
|
}, [api, fetchBlocks]);
|
||||||
|
|
||||||
|
const fetchMoreBlockOfACategory = async (category: string) => {
|
||||||
|
try {
|
||||||
|
setLoadingCategories((prev) => new Set(prev).add(category));
|
||||||
|
const response = await api.getBuilderBlocks({ category: category });
|
||||||
|
const updatedCategories = categories.map((cat) => {
|
||||||
|
if (cat.name === category) {
|
||||||
|
return {
|
||||||
|
...cat,
|
||||||
|
blocks: [...response.blocks],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cat;
|
||||||
|
});
|
||||||
|
|
||||||
|
setCategories(updatedCategories);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch blocks for category ${category}:`, error);
|
||||||
|
} finally {
|
||||||
|
setLoadingCategories((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(category);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{Array.from({ length: 3 }).map((_, categoryIndex) => (
|
||||||
|
<Fragment key={categoryIndex}>
|
||||||
|
{categoryIndex > 0 && (
|
||||||
|
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
|
||||||
|
)}
|
||||||
|
{[0, 1, 2].map((blockIndex) => (
|
||||||
|
<Block.Skeleton key={`${categoryIndex}-${blockIndex}`} />
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load blocks"
|
||||||
|
error={error}
|
||||||
|
onRetry={fetchBlocks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{categories.map((category, index) => (
|
||||||
|
<Fragment key={category.name}>
|
||||||
|
{index > 0 && (
|
||||||
|
<Separator className="h-[1px] w-full text-zinc-300" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Category Section */}
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
{category.name && beautifyString(category.name)}
|
||||||
|
</p>
|
||||||
|
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
|
||||||
|
{category.total_blocks}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{category.blocks.map((block, idx) => (
|
||||||
|
<Block
|
||||||
|
key={`${category.name}-${idx}`}
|
||||||
|
title={block.name}
|
||||||
|
description={block.name}
|
||||||
|
onClick={() => {
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{loadingCategories.has(category.name) && (
|
||||||
|
<>
|
||||||
|
{[0, 1, 2, 3, 4].map((skeletonIndex) => (
|
||||||
|
<Block.Skeleton
|
||||||
|
key={`skeleton-${category.name}-${skeletonIndex}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{category.total_blocks > category.blocks.length && (
|
||||||
|
<Button
|
||||||
|
variant={"link"}
|
||||||
|
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
|
||||||
|
disabled={loadingCategories.has(category.name)}
|
||||||
|
onClick={() => {
|
||||||
|
fetchMoreBlockOfACategory(category.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
see all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AllBlocksContent;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import BlockMenuSidebar from "./BlockMenuSidebar";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import BlockMenuDefaultContent from "./BlockMenuDefaultContent";
|
||||||
|
|
||||||
|
const BlockMenuDefault: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 overflow-y-auto">
|
||||||
|
<BlockMenuSidebar />
|
||||||
|
<Separator className="h-full w-[1px] text-zinc-300" />
|
||||||
|
<BlockMenuDefaultContent />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockMenuDefault;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SuggestionContent from "./SuggestionContent";
|
||||||
|
import AllBlocksContent from "./AllBlocksContent";
|
||||||
|
import IntegrationsContent from "./IntegrationsContent";
|
||||||
|
import MarketplaceAgentsContent from "./MarketplaceAgentsContent";
|
||||||
|
import MyAgentsContent from "./MyAgentsContent";
|
||||||
|
import ActionBlocksContent from "./ActionBlocksContent";
|
||||||
|
import InputBlocksContent from "./InputBlocksContent";
|
||||||
|
import OutputBlocksContent from "./OutputBlocksContent";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
|
||||||
|
export interface ActionBlock {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockListType {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockMenuDefaultContent: React.FC = ({}) => {
|
||||||
|
const { defaultState } = useBlockMenuContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex-1 overflow-hidden">
|
||||||
|
{defaultState == "suggestion" && <SuggestionContent />}
|
||||||
|
{defaultState == "all_blocks" && <AllBlocksContent />}
|
||||||
|
{defaultState == "input_blocks" && <InputBlocksContent />}
|
||||||
|
{defaultState == "action_blocks" && <ActionBlocksContent />}
|
||||||
|
{defaultState == "output_blocks" && <OutputBlocksContent />}
|
||||||
|
{defaultState == "integrations" && <IntegrationsContent />}
|
||||||
|
{defaultState == "marketplace_agents" && <MarketplaceAgentsContent />}
|
||||||
|
{defaultState == "my_agents" && <MyAgentsContent />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockMenuDefaultContent;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import MenuItem from "../MenuItem";
|
||||||
|
import { DefaultStateType, useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { CountResponse } from "@/lib/autogpt-server-api";
|
||||||
|
|
||||||
|
const BlockMenuSidebar: React.FC = ({}) => {
|
||||||
|
const { defaultState, setDefaultState, setIntegration } =
|
||||||
|
useBlockMenuContext();
|
||||||
|
const [blockCounts, setBlockCounts] = useState<CountResponse | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBlockCounts = async () => {
|
||||||
|
try {
|
||||||
|
const counts = await api.getBlockCounts();
|
||||||
|
setBlockCounts(counts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch block counts:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBlockCounts();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const topLevelMenuItems = [
|
||||||
|
{
|
||||||
|
name: "Suggestion",
|
||||||
|
type: "suggestion",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All blocks",
|
||||||
|
type: "all_blocks",
|
||||||
|
number: blockCounts?.all_blocks,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const subMenuItems = [
|
||||||
|
{
|
||||||
|
name: "Input blocks",
|
||||||
|
type: "input_blocks",
|
||||||
|
number: blockCounts?.input_blocks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Action blocks",
|
||||||
|
type: "action_blocks",
|
||||||
|
number: blockCounts?.action_blocks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Output blocks",
|
||||||
|
type: "output_blocks",
|
||||||
|
number: blockCounts?.output_blocks,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const bottomMenuItems = [
|
||||||
|
{
|
||||||
|
name: "Integrations",
|
||||||
|
type: "integrations",
|
||||||
|
number: blockCounts?.integrations,
|
||||||
|
onClick: () => {
|
||||||
|
setIntegration(null);
|
||||||
|
setDefaultState("integrations");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Marketplace Agents",
|
||||||
|
type: "marketplace_agents",
|
||||||
|
number: blockCounts?.marketplace_agents,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "My Agents",
|
||||||
|
type: "my_agents",
|
||||||
|
number: blockCounts?.my_agents,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-fit space-y-2 px-4 pt-4">
|
||||||
|
{topLevelMenuItems.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.type}
|
||||||
|
name={item.name}
|
||||||
|
number={item.number}
|
||||||
|
selected={defaultState === item.type}
|
||||||
|
onClick={() => setDefaultState(item.type as DefaultStateType)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="ml-[0.5365rem] space-y-2 border-l border-black/10 pl-[0.75rem]">
|
||||||
|
{subMenuItems.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.type}
|
||||||
|
name={item.name}
|
||||||
|
number={item.number}
|
||||||
|
className="max-w-[11.5339rem]"
|
||||||
|
selected={defaultState === item.type}
|
||||||
|
onClick={() => setDefaultState(item.type as DefaultStateType)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{bottomMenuItems.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.type}
|
||||||
|
name={item.name}
|
||||||
|
number={item.number}
|
||||||
|
selected={defaultState === item.type}
|
||||||
|
onClick={
|
||||||
|
item.onClick ||
|
||||||
|
(() => setDefaultState(item.type as DefaultStateType))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockMenuSidebar;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Block from "../Block";
|
||||||
|
import { Block as BlockType } from "@/lib/autogpt-server-api";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
|
||||||
|
interface BlocksListProps {
|
||||||
|
blocks: BlockType[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlocksList: React.FC<BlocksListProps> = ({ blocks, loading = false }) => {
|
||||||
|
const { addNode } = useBlockMenuContext();
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{loading
|
||||||
|
? Array.from({ length: 7 }).map((_, index) => (
|
||||||
|
<Block.Skeleton key={index} />
|
||||||
|
))
|
||||||
|
: blocks.map((block) => (
|
||||||
|
<Block
|
||||||
|
key={block.id}
|
||||||
|
title={block.name}
|
||||||
|
description={block.description}
|
||||||
|
onClick={() => {
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlocksList;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||||
|
|
||||||
|
const InputBlocksContent: React.FC = () => {
|
||||||
|
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputBlocksContent;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import React, { useState, useEffect, Fragment, useCallback } from "react";
|
||||||
|
import IntegrationBlock from "../IntegrationBlock";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { Block } from "@/lib/autogpt-server-api";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
const IntegrationBlocks: React.FC = ({}) => {
|
||||||
|
const { integration, setIntegration, addNode } = useBlockMenuContext();
|
||||||
|
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
const fetchBlocks = useCallback(async () => {
|
||||||
|
if (integration) {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await api.getBuilderBlocks({ provider: integration });
|
||||||
|
setBlocks(response.blocks);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch integration blocks:", err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "Failed to load integration blocks",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [api, integration]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlocks();
|
||||||
|
}, [api, integration, fetchBlocks]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-3 p-4">
|
||||||
|
{Array.from({ length: 3 }).map((_, blockIndex) => (
|
||||||
|
<Fragment key={blockIndex}>
|
||||||
|
{blockIndex > 0 && (
|
||||||
|
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
|
||||||
|
)}
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<IntegrationBlock.Skeleton key={`${blockIndex}-${index}`} />
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load integration blocks"
|
||||||
|
error={error}
|
||||||
|
onRetry={fetchBlocks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant={"link"}
|
||||||
|
className="p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800"
|
||||||
|
onClick={() => {
|
||||||
|
setIntegration(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Integrations
|
||||||
|
</Button>
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
/
|
||||||
|
</p>
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
{integration}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="flex h-[1.375rem] w-[1.6875rem] items-center justify-center rounded-[1.25rem] bg-[#f0f0f0] p-1.5 font-sans text-sm leading-[1.375rem] text-zinc-500 group-disabled:text-zinc-400">
|
||||||
|
{blocks.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{blocks.map((block, index) => (
|
||||||
|
<IntegrationBlock
|
||||||
|
key={index}
|
||||||
|
title={block.name}
|
||||||
|
description={block.description}
|
||||||
|
icon_url={`/integrations/${integration}.png`}
|
||||||
|
onClick={() => {
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntegrationBlocks;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PaginatedIntegrationList from "./PaginatedIntegrationList";
|
||||||
|
import IntegrationBlocks from "./IntegrationBlocks";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
|
||||||
|
const IntegrationsContent: React.FC = () => {
|
||||||
|
const { integration } = useBlockMenuContext();
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
return <PaginatedIntegrationList />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
|
||||||
|
<div className="w-full px-4 pb-4">
|
||||||
|
<IntegrationBlocks />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntegrationsContent;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from "react";
|
||||||
|
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
|
||||||
|
import { usePagination } from "@/hooks/usePagination";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
|
||||||
|
const MarketplaceAgentsContent: React.FC = () => {
|
||||||
|
const {
|
||||||
|
data: agents,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
error,
|
||||||
|
scrollRef,
|
||||||
|
refresh,
|
||||||
|
} = usePagination({
|
||||||
|
request: { apiType: "store-agents" },
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const { handleAddStoreAgent, loadingSlug } = useBlockMenuContext();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<MarketplaceAgentBlock.Skeleton key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load marketplace agents"
|
||||||
|
error={error}
|
||||||
|
onRetry={refresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<MarketplaceAgentBlock
|
||||||
|
key={agent.slug}
|
||||||
|
slug={agent.slug}
|
||||||
|
title={agent.agent_name}
|
||||||
|
image_url={agent.agent_image}
|
||||||
|
creator_name={agent.creator}
|
||||||
|
number_of_runs={agent.runs}
|
||||||
|
loading={loadingSlug === agent.slug}
|
||||||
|
onClick={() =>
|
||||||
|
handleAddStoreAgent({
|
||||||
|
creator_name: agent.creator,
|
||||||
|
slug: agent.slug,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{loadingMore && hasMore && (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<MarketplaceAgentBlock.Skeleton key={`loading-${index}`} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketplaceAgentsContent;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import UGCAgentBlock from "../UGCAgentBlock";
|
||||||
|
import { usePagination } from "@/hooks/usePagination";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import { convertLibraryAgentIntoBlock } from "@/lib/utils";
|
||||||
|
|
||||||
|
const MyAgentsContent: React.FC = () => {
|
||||||
|
const {
|
||||||
|
data: agents,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
error,
|
||||||
|
scrollRef,
|
||||||
|
refresh,
|
||||||
|
} = usePagination({
|
||||||
|
request: { apiType: "library-agents" },
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const { addNode } = useBlockMenuContext();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<UGCAgentBlock.Skeleton key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load library agents"
|
||||||
|
error={error}
|
||||||
|
onRetry={refresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<UGCAgentBlock
|
||||||
|
key={agent.id}
|
||||||
|
title={agent.name}
|
||||||
|
edited_time={agent.updated_at}
|
||||||
|
version={agent.graph_version}
|
||||||
|
image_url={agent.image_url}
|
||||||
|
onClick={() => {
|
||||||
|
const block = convertLibraryAgentIntoBlock(agent);
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{loadingMore && hasMore && (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<UGCAgentBlock.Skeleton key={`loading-${index}`} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyAgentsContent;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||||
|
|
||||||
|
const OutputBlocksContent: React.FC = () => {
|
||||||
|
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OutputBlocksContent;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { Fragment } from "react";
|
||||||
|
import BlocksList from "./BlocksList";
|
||||||
|
import Block from "../Block";
|
||||||
|
import { BlockRequest } from "@/lib/autogpt-server-api";
|
||||||
|
import { usePagination } from "@/hooks/usePagination";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
|
||||||
|
interface PaginatedBlocksContentProps {
|
||||||
|
blockRequest: BlockRequest;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginatedBlocksContent: React.FC<PaginatedBlocksContentProps> = ({
|
||||||
|
blockRequest,
|
||||||
|
pageSize = 10,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: blocks,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
error,
|
||||||
|
scrollRef,
|
||||||
|
refresh,
|
||||||
|
} = usePagination({
|
||||||
|
request: { apiType: "blocks", ...blockRequest },
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full px-4 pb-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load blocks"
|
||||||
|
error={error}
|
||||||
|
onRetry={refresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<BlocksList blocks={blocks} loading={loading} />
|
||||||
|
{loadingMore && hasMore && (
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<Block.Skeleton key={`loading-${index}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaginatedBlocksContent;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Integration from "../Integration";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import { usePagination } from "@/hooks/usePagination";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
|
||||||
|
const PaginatedIntegrationList: React.FC = () => {
|
||||||
|
const { setIntegration } = useBlockMenuContext();
|
||||||
|
const {
|
||||||
|
data: providers,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
error,
|
||||||
|
scrollRef,
|
||||||
|
refresh,
|
||||||
|
} = usePagination({
|
||||||
|
request: { apiType: "providers" },
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<div className="w-full space-y-3 px-4 pb-4">
|
||||||
|
{Array.from({ length: 6 }).map((_, integrationIndex) => (
|
||||||
|
<Integration.Skeleton key={integrationIndex} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load integrations"
|
||||||
|
error={error}
|
||||||
|
onRetry={refresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
<div className="w-full px-4 pb-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{providers.map((integration, index) => (
|
||||||
|
<Integration
|
||||||
|
key={index}
|
||||||
|
title={integration.name}
|
||||||
|
icon_url={`/integrations/${integration.name}.png`}
|
||||||
|
description={integration.description}
|
||||||
|
number_of_blocks={integration.integration_count}
|
||||||
|
onClick={() => setIntegration(integration.name)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{loadingMore && hasMore && (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<Integration.Skeleton key={`loading-${index}`} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaginatedIntegrationList;
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import SearchHistoryChip from "../SearchHistoryChip";
|
||||||
|
import IntegrationChip from "../IntegrationChip";
|
||||||
|
import Block from "../Block";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import {
|
||||||
|
CredentialsProviderName,
|
||||||
|
SuggestionsResponse,
|
||||||
|
} from "@/lib/autogpt-server-api";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import ErrorState from "../ErrorState";
|
||||||
|
|
||||||
|
const SuggestionContent: React.FC = () => {
|
||||||
|
const { setIntegration, setDefaultState, setSearchQuery, addNode } =
|
||||||
|
useBlockMenuContext();
|
||||||
|
|
||||||
|
const [suggestionsData, setSuggestionsData] =
|
||||||
|
useState<SuggestionsResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
const fetchSuggestions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await api.getSuggestions();
|
||||||
|
setSuggestionsData(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching data:", err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load suggestions",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSuggestions();
|
||||||
|
}, [api, fetchSuggestions]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4">
|
||||||
|
<ErrorState
|
||||||
|
title="Failed to load suggestions"
|
||||||
|
error={error}
|
||||||
|
onRetry={fetchSuggestions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
|
||||||
|
<div className="w-full space-y-6 pb-4">
|
||||||
|
{/* Recent Searches */}
|
||||||
|
{/* <div className="-mb-2 space-y-2.5">
|
||||||
|
<p className="px-4 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
Recent searches
|
||||||
|
</p>
|
||||||
|
<div className="scrollbar-thumb-rounded flex flex-nowrap gap-2 overflow-x-auto transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
|
||||||
|
{" "}
|
||||||
|
{!loading && suggestionsData
|
||||||
|
? suggestionsData.recent_searches.map((search, index) => (
|
||||||
|
<SearchHistoryChip
|
||||||
|
key={`search-${index}`}
|
||||||
|
content={search}
|
||||||
|
className={index === 0 ? "ml-4" : ""}
|
||||||
|
onClick={() => setSearchQuery(search)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => (
|
||||||
|
<SearchHistoryChip.Skeleton
|
||||||
|
key={`search-${index}`}
|
||||||
|
className={index === 0 ? "ml-4" : ""}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!loading && suggestionsData
|
||||||
|
? suggestionsData.recent_searches.map((search, index) => (
|
||||||
|
<SearchHistoryChip
|
||||||
|
key={`search-${index}`}
|
||||||
|
content={search}
|
||||||
|
className={index === 0 ? "ml-4" : ""}
|
||||||
|
onClick={() => setSearchQuery(search)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => (
|
||||||
|
<SearchHistoryChip.Skeleton
|
||||||
|
key={`search-${index}`}
|
||||||
|
className={index === 0 ? "ml-4" : ""}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Integrations */}
|
||||||
|
<div className="space-y-2.5 px-4">
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
Integrations
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 grid-rows-2 gap-2">
|
||||||
|
{!loading && suggestionsData
|
||||||
|
? suggestionsData.providers.map((provider, index) => (
|
||||||
|
<IntegrationChip
|
||||||
|
key={`integration-${index}`}
|
||||||
|
icon_url={`/integrations/${provider}.png`}
|
||||||
|
name={provider}
|
||||||
|
onClick={() => {
|
||||||
|
setDefaultState("integrations");
|
||||||
|
setIntegration(provider as CredentialsProviderName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: Array(6)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => (
|
||||||
|
<IntegrationChip.Skeleton
|
||||||
|
key={`integration-skeleton-${index}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top blocks */}
|
||||||
|
<div className="space-y-2.5 px-4">
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
Top blocks
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{!loading && suggestionsData
|
||||||
|
? suggestionsData.top_blocks.map((block, index) => (
|
||||||
|
<Block
|
||||||
|
key={`block-${index}`}
|
||||||
|
title={block.name}
|
||||||
|
description={block.description}
|
||||||
|
onClick={() => {
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => (
|
||||||
|
<Block.Skeleton key={`block-skeleton-${index}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestionContent;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { ButtonHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
ai_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AiBlock: React.FC<Props> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
ai_name,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"group flex h-[5.625rem] w-full min-w-[7.5rem] items-center justify-start space-x-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
|
||||||
|
"hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 flex-col items-start gap-1.5">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-700 group-disabled:text-zinc-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-[0.75rem] bg-zinc-200 px-[0.5rem] font-sans text-xs leading-[1.25rem] text-zinc-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Supports {ai_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AiBlock;
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||||
|
import FiltersList from "./FiltersList";
|
||||||
|
import SearchList from "./SearchList";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
|
||||||
|
const BlockMenuSearch: React.FC = ({}) => {
|
||||||
|
const {
|
||||||
|
searchData,
|
||||||
|
searchQuery,
|
||||||
|
searchId,
|
||||||
|
setSearchData,
|
||||||
|
filters,
|
||||||
|
setCategoryCounts,
|
||||||
|
} = useBlockMenuContext();
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const fetchSearchData = useCallback(
|
||||||
|
async (pageNum: number, isLoadMore: boolean = false) => {
|
||||||
|
if (isLoadMore) {
|
||||||
|
setLoadingMore(true);
|
||||||
|
} else {
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activeCategories = Object.entries(filters.categories)
|
||||||
|
.filter(([_, isActive]) => isActive)
|
||||||
|
.map(([category, _]) => category)
|
||||||
|
.map(
|
||||||
|
(category) =>
|
||||||
|
category as
|
||||||
|
| "blocks"
|
||||||
|
| "integrations"
|
||||||
|
| "marketplace_agents"
|
||||||
|
| "my_agents",
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await api.searchBlocks({
|
||||||
|
search_query: searchQuery,
|
||||||
|
search_id: searchId,
|
||||||
|
page: pageNum,
|
||||||
|
page_size: pageSize,
|
||||||
|
filter: activeCategories.length > 0 ? activeCategories : undefined,
|
||||||
|
by_creator:
|
||||||
|
filters.createdBy.length > 0 ? filters.createdBy : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
setCategoryCounts(response.total_items);
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
setSearchData((prev) => [...prev, ...response.items]);
|
||||||
|
} else {
|
||||||
|
setSearchData(response.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasMore(response.more_pages);
|
||||||
|
setError(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching search data:", error);
|
||||||
|
setError(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to load search results",
|
||||||
|
);
|
||||||
|
if (!isLoadMore) {
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setLoadingMore(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
searchQuery,
|
||||||
|
searchId,
|
||||||
|
filters,
|
||||||
|
api,
|
||||||
|
setCategoryCounts,
|
||||||
|
setSearchData,
|
||||||
|
pageSize,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!scrollRef.current || loadingMore || !hasMore) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||||
|
const nextPage = page + 1;
|
||||||
|
setPage(nextPage);
|
||||||
|
fetchSearchData(nextPage, true);
|
||||||
|
}
|
||||||
|
}, [loadingMore, hasMore, page, fetchSearchData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollElement = scrollRef.current;
|
||||||
|
if (scrollElement) {
|
||||||
|
scrollElement.addEventListener("scroll", handleScroll);
|
||||||
|
return () => scrollElement.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchQuery) {
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
setError(null);
|
||||||
|
fetchSearchData(1, false);
|
||||||
|
} else {
|
||||||
|
setSearchData([]);
|
||||||
|
setError(null);
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
}
|
||||||
|
}, [searchQuery, searchId, filters, fetchSearchData, setSearchData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="scrollbar-thumb-rounded h-full space-y-4 overflow-y-auto py-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
|
||||||
|
>
|
||||||
|
{searchData.length !== 0 && <FiltersList />}
|
||||||
|
<SearchList
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingMore={loadingMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
error={error}
|
||||||
|
onRetry={() => {
|
||||||
|
setPage(1);
|
||||||
|
setError(null);
|
||||||
|
fetchSearchData(1, false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockMenuSearch;
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import FilterChip from "../FilterChip";
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { cn, getBlockType } from "@/lib/utils";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
CategoryKey,
|
||||||
|
Filters,
|
||||||
|
useBlockMenuContext,
|
||||||
|
} from "../block-menu-provider";
|
||||||
|
import { StoreAgent } from "@/lib/autogpt-server-api";
|
||||||
|
|
||||||
|
const INITIAL_CREATORS_TO_SHOW = 5;
|
||||||
|
|
||||||
|
export default function FilterSheet({
|
||||||
|
categories,
|
||||||
|
}: {
|
||||||
|
categories: Array<{ key: CategoryKey; name: string }>;
|
||||||
|
}) {
|
||||||
|
const { filters, setFilters, searchData } = useBlockMenuContext();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isSheetVisible, setIsSheetVisible] = useState(false);
|
||||||
|
const [localFilters, setLocalFilters] = useState<Filters>(filters);
|
||||||
|
|
||||||
|
const [creators, setCreators] = useState<string[]>([]);
|
||||||
|
const [displayedCreatorsCount, setDisplayedCreatorsCount] = useState(
|
||||||
|
INITIAL_CREATORS_TO_SHOW,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsSheetVisible(true);
|
||||||
|
setLocalFilters(filters);
|
||||||
|
setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW); // Reset on open
|
||||||
|
|
||||||
|
const marketplaceAgents = (searchData?.filter(
|
||||||
|
(item) => getBlockType(item) === "store_agent",
|
||||||
|
) || []) as StoreAgent[];
|
||||||
|
|
||||||
|
const uniqueCreators = Array.from(
|
||||||
|
new Set(marketplaceAgents.map((agent) => agent.creator)),
|
||||||
|
);
|
||||||
|
|
||||||
|
setCreators(uniqueCreators);
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsSheetVisible(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen, filters, searchData]);
|
||||||
|
|
||||||
|
const onCategoryChange = useCallback((category: CategoryKey) => {
|
||||||
|
setLocalFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
categories: {
|
||||||
|
...prev.categories,
|
||||||
|
[category]: !prev.categories[category],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCreatorChange = useCallback((creator: string) => {
|
||||||
|
setLocalFilters((prev) => {
|
||||||
|
const updatedCreators = prev.createdBy.includes(creator)
|
||||||
|
? prev.createdBy.filter((c) => c !== creator)
|
||||||
|
: [...prev.createdBy, creator];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
createdBy: updatedCreators,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleApplyFilters = useCallback(() => {
|
||||||
|
setFilters(localFilters);
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [localFilters, setFilters]);
|
||||||
|
|
||||||
|
const handleClearFilters = useCallback(() => {
|
||||||
|
const clearedFilters: Filters = {
|
||||||
|
categories: {
|
||||||
|
blocks: false,
|
||||||
|
integrations: false,
|
||||||
|
marketplace_agents: false,
|
||||||
|
my_agents: false,
|
||||||
|
providers: false,
|
||||||
|
},
|
||||||
|
createdBy: [],
|
||||||
|
};
|
||||||
|
setFilters(clearedFilters);
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [setFilters]);
|
||||||
|
|
||||||
|
const hasLocalActiveFilters = useCallback(() => {
|
||||||
|
const hasCategoryFilter = Object.values(localFilters.categories).some(
|
||||||
|
(value) => value,
|
||||||
|
);
|
||||||
|
const hasCreatorFilter = localFilters.createdBy.length > 0;
|
||||||
|
|
||||||
|
return hasCategoryFilter || hasCreatorFilter;
|
||||||
|
}, [localFilters]);
|
||||||
|
|
||||||
|
const hasActiveFilters = useCallback(() => {
|
||||||
|
const hasCategoryFilter = Object.values(filters.categories).some(
|
||||||
|
(value) => value,
|
||||||
|
);
|
||||||
|
const hasCreatorFilter = filters.createdBy.length > 0;
|
||||||
|
|
||||||
|
return hasCategoryFilter || hasCreatorFilter;
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
const handleToggleShowMoreCreators = () => {
|
||||||
|
if (displayedCreatorsCount < creators.length) {
|
||||||
|
setDisplayedCreatorsCount(creators.length);
|
||||||
|
} else {
|
||||||
|
setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleCreators = creators.slice(0, displayedCreatorsCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-0 ml-4 inline w-fit p-0">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsSheetVisible(true);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant={"link"}
|
||||||
|
className="m-0 p-0 hover:no-underline"
|
||||||
|
>
|
||||||
|
<FilterChip
|
||||||
|
name={hasActiveFilters() ? "Edit filters" : "All filters"}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isSheetVisible && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute bottom-2 left-2 top-2 z-20 w-3/4 max-w-[22.5rem] space-y-4 overflow-hidden rounded-[0.75rem] bg-white pb-4 shadow-[0_4px_12px_2px_rgba(0,0,0,0.1)] transition-all",
|
||||||
|
isOpen
|
||||||
|
? "translate-x-0 duration-300 ease-out"
|
||||||
|
: "-translate-x-full duration-300 ease-out",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 space-y-4 pb-16",
|
||||||
|
"scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Top */}
|
||||||
|
<div className="flex items-center justify-between px-5">
|
||||||
|
<p className="font-sans text-base text-[#040404]">Filters</p>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="h-[1px] w-full text-zinc-300" />
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
|
||||||
|
<div className="space-y-4 px-5">
|
||||||
|
<p className="font-sans text-base font-medium text-zinc-800">
|
||||||
|
Categories
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.key}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={category.key}
|
||||||
|
checked={localFilters.categories[category.key]}
|
||||||
|
onCheckedChange={() => onCategoryChange(category.key)}
|
||||||
|
className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={category.key}
|
||||||
|
className="font-sans text-sm leading-[1.375rem] text-zinc-600"
|
||||||
|
>
|
||||||
|
{category.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="h-[1px] w-full text-zinc-300" />
|
||||||
|
|
||||||
|
{/* Created By */}
|
||||||
|
|
||||||
|
<div className="space-y-4 px-5">
|
||||||
|
<p className="font-sans text-base font-medium text-zinc-800">
|
||||||
|
Created by
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{visibleCreators.map((creator) => (
|
||||||
|
<div key={creator} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`creator-${creator}`}
|
||||||
|
checked={localFilters.createdBy.includes(creator)}
|
||||||
|
onCheckedChange={() => onCreatorChange(creator)}
|
||||||
|
className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`creator-${creator}`}
|
||||||
|
className="font-sans text-sm leading-[1.375rem] text-zinc-600"
|
||||||
|
>
|
||||||
|
{creator}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{creators.length > INITIAL_CREATORS_TO_SHOW && (
|
||||||
|
<Button
|
||||||
|
variant={"link"}
|
||||||
|
className="m-0 p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 underline hover:text-zinc-600"
|
||||||
|
onClick={handleToggleShowMoreCreators}
|
||||||
|
>
|
||||||
|
{displayedCreatorsCount < creators.length ? "More" : "Less"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer buttons */}
|
||||||
|
<div className="fixed bottom-0 flex w-full justify-between gap-3 border-t border-zinc-300 bg-white px-5 py-3">
|
||||||
|
<Button
|
||||||
|
className="min-w-[5rem] rounded-[0.5rem] border-none px-1.5 py-2 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 shadow-none ring-1 ring-zinc-400"
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={handleClearFilters}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"min-w-[6.25rem] rounded-[0.5rem] border-none px-1.5 py-2 font-sans text-sm font-medium leading-[1.375rem] text-white shadow-none ring-1 disabled:ring-0",
|
||||||
|
)}
|
||||||
|
onClick={handleApplyFilters}
|
||||||
|
disabled={!hasLocalActiveFilters()}
|
||||||
|
>
|
||||||
|
Apply filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import FilterChip from "../FilterChip";
|
||||||
|
import FilterSheet from "./FilterSheet";
|
||||||
|
import { CategoryKey, useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
|
||||||
|
const FiltersList = () => {
|
||||||
|
const { filters, setFilters, categoryCounts } = useBlockMenuContext();
|
||||||
|
const categories: Array<{ key: CategoryKey; name: string }> = [
|
||||||
|
{ key: "blocks", name: "Blocks" },
|
||||||
|
{ key: "integrations", name: "Integrations" },
|
||||||
|
{ key: "marketplace_agents", name: "Marketplace agents" },
|
||||||
|
{ key: "my_agents", name: "My agents" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleCategoryFilter = (category: CategoryKey) => {
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
categories: {
|
||||||
|
...filters.categories,
|
||||||
|
[category]: !filters.categories[category],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatorFilter = useCallback(
|
||||||
|
(creator: string) => {
|
||||||
|
const updatedCreators = filters.createdBy.includes(creator)
|
||||||
|
? filters.createdBy.filter((c) => c !== creator)
|
||||||
|
: [...filters.createdBy, creator];
|
||||||
|
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
createdBy: updatedCreators,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[filters, setFilters],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-nowrap gap-3 overflow-x-auto scrollbar-hide">
|
||||||
|
<FilterSheet categories={categories} />
|
||||||
|
|
||||||
|
{filters.createdBy.map((creator) => (
|
||||||
|
<FilterChip
|
||||||
|
key={creator}
|
||||||
|
name={"Created by " + creator}
|
||||||
|
selected={true}
|
||||||
|
onClick={() => handleCreatorFilter(creator)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{categories.map((category) => (
|
||||||
|
<FilterChip
|
||||||
|
key={category.key}
|
||||||
|
name={category.name}
|
||||||
|
number={categoryCounts[category.key]}
|
||||||
|
selected={filters.categories[category.key]}
|
||||||
|
onClick={() => handleCategoryFilter(category.key)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FiltersList;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Frown } from "lucide-react";
|
||||||
|
|
||||||
|
const NoSearchResult = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center text-center">
|
||||||
|
<Frown className="mb-10 h-16 w-16 text-zinc-400" strokeWidth={1} />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
No match found
|
||||||
|
</p>
|
||||||
|
<p className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-600">
|
||||||
|
Try adjusting your search terms
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoSearchResult;
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import React from "react";
|
||||||
|
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
|
||||||
|
import Block from "../Block";
|
||||||
|
import UGCAgentBlock from "../UGCAgentBlock";
|
||||||
|
import AiBlock from "./AiBlock";
|
||||||
|
import IntegrationBlock from "../IntegrationBlock";
|
||||||
|
import { useBlockMenuContext } from "../block-menu-provider";
|
||||||
|
import NoSearchResult from "./NoSearchResult";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { convertLibraryAgentIntoBlock, getBlockType } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface SearchListProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
loadingMore: boolean;
|
||||||
|
hasMore: boolean;
|
||||||
|
error: string | null;
|
||||||
|
onRetry: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchList: React.FC<SearchListProps> = ({
|
||||||
|
isLoading,
|
||||||
|
loadingMore,
|
||||||
|
hasMore,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
}) => {
|
||||||
|
const { searchQuery, addNode, loadingSlug, searchData, handleAddStoreAgent } =
|
||||||
|
useBlockMenuContext();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2.5 px-4">
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
Search results
|
||||||
|
</p>
|
||||||
|
{Array(6)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<Block.Skeleton key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||||
|
<p className="mb-2 text-sm text-red-600">
|
||||||
|
Error loading search results: {error}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onRetry}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchData.length === 0) {
|
||||||
|
return <NoSearchResult />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2.5 px-4">
|
||||||
|
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||||
|
Search results
|
||||||
|
</p>
|
||||||
|
{searchData.map((item: any, index: number) => {
|
||||||
|
const blockType = getBlockType(item);
|
||||||
|
|
||||||
|
switch (blockType) {
|
||||||
|
case "store_agent":
|
||||||
|
return (
|
||||||
|
<MarketplaceAgentBlock
|
||||||
|
key={index}
|
||||||
|
slug={item.slug}
|
||||||
|
highlightedText={searchQuery}
|
||||||
|
title={item.agent_name}
|
||||||
|
image_url={item.agent_image}
|
||||||
|
creator_name={item.creator}
|
||||||
|
number_of_runs={item.runs}
|
||||||
|
loading={loadingSlug == item.slug}
|
||||||
|
onClick={() =>
|
||||||
|
handleAddStoreAgent({
|
||||||
|
creator_name: item.creator,
|
||||||
|
slug: item.slug,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "block":
|
||||||
|
return (
|
||||||
|
<Block
|
||||||
|
key={index}
|
||||||
|
title={item.name}
|
||||||
|
highlightedText={searchQuery}
|
||||||
|
description={item.description}
|
||||||
|
onClick={() => {
|
||||||
|
addNode(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "provider":
|
||||||
|
return (
|
||||||
|
<IntegrationBlock
|
||||||
|
key={index}
|
||||||
|
title={item.name}
|
||||||
|
highlightedText={searchQuery}
|
||||||
|
icon_url={`/integrations/${item.name}.png`}
|
||||||
|
description={item.description}
|
||||||
|
onClick={() => {
|
||||||
|
addNode(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "library_agent":
|
||||||
|
return (
|
||||||
|
<UGCAgentBlock
|
||||||
|
key={index}
|
||||||
|
title={item.name}
|
||||||
|
highlightedText={searchQuery}
|
||||||
|
image_url={item.image_url}
|
||||||
|
version={item.graph_version}
|
||||||
|
edited_time={item.updated_at}
|
||||||
|
onClick={() => {
|
||||||
|
const block = convertLibraryAgentIntoBlock(item);
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "ai_agent":
|
||||||
|
return (
|
||||||
|
<AiBlock
|
||||||
|
key={index}
|
||||||
|
title={item.name}
|
||||||
|
description={item.description}
|
||||||
|
ai_name={item.inputSchema.properties.model.enum.find(
|
||||||
|
(model: string) =>
|
||||||
|
model
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase().trim()),
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
const block = convertLibraryAgentIntoBlock(item);
|
||||||
|
addNode(block);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{loadingMore && hasMore && (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<Block.Skeleton key={`loading-more-${i}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchList;
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a control element for the ControlPanel Component.
|
* Represents a control element for the ControlPanel Component.
|
||||||
@@ -27,6 +21,7 @@ interface ControlPanelProps {
|
|||||||
controls: Control[];
|
controls: Control[];
|
||||||
topChildren?: React.ReactNode;
|
topChildren?: React.ReactNode;
|
||||||
botChildren?: React.ReactNode;
|
botChildren?: React.ReactNode;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,42 +40,31 @@ export const ControlPanel = ({
|
|||||||
className,
|
className,
|
||||||
}: ControlPanelProps) => {
|
}: ControlPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<Card className={cn("m-4 mt-24 w-14 dark:bg-slate-900", className)}>
|
<section
|
||||||
<CardContent className="p-0">
|
className={cn(
|
||||||
<div className="flex flex-col items-center gap-3 rounded-xl py-3">
|
"absolute left-4 top-24 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
|
||||||
{topChildren}
|
className,
|
||||||
<Separator className="dark:bg-slate-700" />
|
)}
|
||||||
{controls.map((control, index) => (
|
>
|
||||||
<Tooltip key={index} delayDuration={500}>
|
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
|
||||||
<TooltipTrigger asChild>
|
{topChildren}
|
||||||
<div>
|
<Separator className="text-[#E1E1E1]" />
|
||||||
<Button
|
{controls.map((control, index) => (
|
||||||
variant="ghost"
|
<ControlPanelButton
|
||||||
size="icon"
|
key={index}
|
||||||
onClick={() => control.onClick()}
|
onClick={() => control.onClick()}
|
||||||
data-id={`control-button-${index}`}
|
data-id={`control-button-${index}`}
|
||||||
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
|
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
|
||||||
disabled={control.disabled || false}
|
disabled={control.disabled || false}
|
||||||
className="dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
|
className="rounded-none"
|
||||||
>
|
>
|
||||||
{control.icon}
|
{control.icon}
|
||||||
<span className="sr-only">{control.label}</span>
|
</ControlPanelButton>
|
||||||
</Button>
|
))}
|
||||||
</div>
|
<Separator className="text-[#E1E1E1]" />
|
||||||
</TooltipTrigger>
|
{botChildren}
|
||||||
<TooltipContent
|
</div>
|
||||||
side="right"
|
</section>
|
||||||
className="dark:bg-slate-800 dark:text-slate-100"
|
|
||||||
>
|
|
||||||
{control.label}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
<Separator className="dark:bg-slate-700" />
|
|
||||||
{botChildren}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default ControlPanel;
|
export default ControlPanel;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
|
||||||
|
|
||||||
interface SaveControlProps {
|
interface SaveControlProps {
|
||||||
agentMeta: GraphMeta | null;
|
agentMeta: GraphMeta | null;
|
||||||
@@ -26,6 +27,11 @@ interface SaveControlProps {
|
|||||||
onNameChange: (name: string) => void;
|
onNameChange: (name: string) => void;
|
||||||
onDescriptionChange: (description: string) => void;
|
onDescriptionChange: (description: string) => void;
|
||||||
pinSavePopover: boolean;
|
pinSavePopover: boolean;
|
||||||
|
|
||||||
|
blockMenuSelected: "save" | "block" | "";
|
||||||
|
setBlockMenuSelected: React.Dispatch<
|
||||||
|
React.SetStateAction<"" | "save" | "block">
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +54,8 @@ export const SaveControl = ({
|
|||||||
onNameChange,
|
onNameChange,
|
||||||
agentDescription,
|
agentDescription,
|
||||||
onDescriptionChange,
|
onDescriptionChange,
|
||||||
|
blockMenuSelected,
|
||||||
|
setBlockMenuSelected,
|
||||||
pinSavePopover,
|
pinSavePopover,
|
||||||
}: SaveControlProps) => {
|
}: SaveControlProps) => {
|
||||||
/**
|
/**
|
||||||
@@ -82,27 +90,29 @@ export const SaveControl = ({
|
|||||||
}, [handleSave, toast]);
|
}, [handleSave, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={pinSavePopover ? true : undefined}>
|
<Popover
|
||||||
<Tooltip delayDuration={500}>
|
open={pinSavePopover ? true : undefined}
|
||||||
<TooltipTrigger asChild>
|
onOpenChange={(open) => open || setBlockMenuSelected("")}
|
||||||
<PopoverTrigger asChild>
|
>
|
||||||
<Button
|
<PopoverTrigger>
|
||||||
variant="ghost"
|
<ControlPanelButton
|
||||||
size="icon"
|
data-id="save-control-popover-trigger"
|
||||||
data-id="save-control-popover-trigger"
|
data-testid="blocks-control-save-button"
|
||||||
data-testid="blocks-control-save-button"
|
selected={blockMenuSelected === "save"}
|
||||||
name="Save"
|
onClick={() => {
|
||||||
>
|
setBlockMenuSelected("save");
|
||||||
<IconSave className="dark:text-gray-300" />
|
}}
|
||||||
</Button>
|
className="rounded-none"
|
||||||
</PopoverTrigger>
|
>
|
||||||
</TooltipTrigger>
|
<IconSave className="h-5 w-5" strokeWidth={2} />
|
||||||
<TooltipContent side="right">Save</TooltipContent>
|
</ControlPanelButton>
|
||||||
</Tooltip>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
side="right"
|
side="right"
|
||||||
sideOffset={15}
|
sideOffset={16}
|
||||||
align="start"
|
align="start"
|
||||||
|
className="w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
|
||||||
data-id="save-control-popover-content"
|
data-id="save-control-popover-content"
|
||||||
>
|
>
|
||||||
<Card className="border-none shadow-none dark:bg-slate-900">
|
<Card className="border-none shadow-none dark:bg-slate-900">
|
||||||
|
|||||||
Reference in New Issue
Block a user