mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): redesign-block-menu-part-2 (#10793)
In this PR, I have added: - a search input - conditional rendering of the search page and the default page - a sidebar for the default page (with the correct data) ### Screenshot <img width="1512" height="982" alt="Screenshot 2025-09-01 at 12 28 34 PM" src="https://github.com/user-attachments/assets/891ab99f-dde5-47b8-a980-a700845f10c2" /> #### Checklist: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Everything works perfectly locally.
This commit is contained in:
@@ -8,6 +8,7 @@ import { ToyBrick } from "lucide-react";
|
||||
import { BlockMenuContent } from "../BlockMenuContent/BlockMenuContent";
|
||||
import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { useBlockMenu } from "./useBlockMenu";
|
||||
import { BlockMenuStateProvider } from "../block-menu-provider";
|
||||
|
||||
interface BlockMenuProps {
|
||||
pinBlocksPopover: boolean;
|
||||
@@ -44,7 +45,10 @@ 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"
|
||||
>
|
||||
<BlockMenuContent />
|
||||
<BlockMenuStateProvider>
|
||||
<BlockMenuContent />
|
||||
</BlockMenuStateProvider>
|
||||
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { BlockMenuSearchBar } from "../BlockMenuSearchBar/BlockMenuSearchBar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { BlockMenuDefault } from "../BlockMenuDefault/BlockMenuDefault";
|
||||
import { BlockMenuSearch } from "../BlockMenuSearch/BlockMenuSearch";
|
||||
|
||||
export const BlockMenuContent = () => {
|
||||
const { searchQuery } = useBlockMenuContext();
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
This is the block menu content
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<BlockMenuSearchBar />
|
||||
<Separator className="h-[1px] w-full text-zinc-300" />
|
||||
{searchQuery ? <BlockMenuSearch /> : <BlockMenuDefault />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { BlockMenuDefaultContent } from "../BlockMenuDefaultContent/BlockMenuDefaultContent";
|
||||
import { BlockMenuSidebar } from "../BlockMenuSidebar/BlockMenuSidebar";
|
||||
|
||||
export const BlockMenuDefault = () => {
|
||||
return (
|
||||
<div className="flex flex-1 overflow-y-auto">
|
||||
<BlockMenuSidebar />
|
||||
<Separator className="h-full w-[1px] text-zinc-300" />
|
||||
<BlockMenuDefaultContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import React from "react";
|
||||
|
||||
export const BlockMenuDefaultContent = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full flex-1 overflow-hidden flex items-center justify-center">
|
||||
{/* I have added temporary content here, will fillup it in follow up prs */}
|
||||
<Text variant="body" className="text-green-300">
|
||||
This is the block menu default content
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export const BlockMenuSearch = () => {
|
||||
return (
|
||||
// This is just a temporary text, will content inside in it [in follow-up prs]
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Text variant="h3" className="text-green-300">
|
||||
This is the block menu search
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useBlockMenuSearchBar } from "./useBlockMenuSearchBar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MagnifyingGlassIcon, XIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface BlockMenuSearchBarProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
|
||||
className = "",
|
||||
}) => {
|
||||
const { handleClear, inputRef, localQuery, setLocalQuery, debouncedSetSearchQuery } = useBlockMenuSearchBar();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-[3.5625rem] items-center gap-2.5 px-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
<MagnifyingGlassIcon className="h-6 w-6 text-zinc-700" strokeWidth={2} />
|
||||
</div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={localQuery}
|
||||
onChange={(e) => {
|
||||
setLocalQuery(e.target.value);
|
||||
debouncedSetSearchQuery(e.target.value);
|
||||
}}
|
||||
placeholder={"Blocks, Agents, Integrations or Keywords..."}
|
||||
className={cn(
|
||||
"m-0 border-none p-0 font-sans text-base font-normal text-zinc-800 shadow-none outline-none",
|
||||
"placeholder:text-zinc-400 focus:shadow-none focus:outline-none focus:ring-0",
|
||||
)}
|
||||
/>
|
||||
{localQuery.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={"sm"}
|
||||
onClick={handleClear}
|
||||
className="p-0 hover:bg-transparent"
|
||||
>
|
||||
<XIcon className="h-6 w-6 text-zinc-700" strokeWidth={2} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { debounce } from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
export const useBlockMenuSearchBar = () => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [localQuery, setLocalQuery] = useState("");
|
||||
const { setSearchQuery, setSearchId, searchId } = useBlockMenuContext();
|
||||
|
||||
const searchIdRef = useRef(searchId);
|
||||
useEffect(() => {
|
||||
searchIdRef.current = searchId;
|
||||
}, [searchId]);
|
||||
|
||||
const debouncedSetSearchQuery = debounce((value: string) => {
|
||||
setSearchQuery(value);
|
||||
if (value.length === 0) {
|
||||
setSearchId(undefined);
|
||||
} else if (!searchIdRef.current) {
|
||||
setSearchId(crypto.randomUUID());
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
}, [debouncedSetSearchQuery]);
|
||||
|
||||
const handleClear = () => {
|
||||
setLocalQuery("");
|
||||
setSearchQuery("");
|
||||
setSearchId(undefined);
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
|
||||
return {
|
||||
handleClear,
|
||||
inputRef,
|
||||
localQuery,
|
||||
setLocalQuery,
|
||||
debouncedSetSearchQuery,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from "react";
|
||||
import { MenuItem } from "../MenuItem";
|
||||
import { DefaultStateType } from "../block-menu-provider";
|
||||
import { useBlockMenuSidebar } from "./useBlockMenuSidebar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
|
||||
export const BlockMenuSidebar = () => {
|
||||
const { blockCounts, setDefaultState, defaultState, isLoading, isError, error } = useBlockMenuSidebar();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-fit space-y-2 px-4 pt-4">
|
||||
<Skeleton className="h-12 w-[12.875rem]" />
|
||||
<Skeleton className="h-12 w-[12.875rem]" />
|
||||
<Skeleton className="h-12 w-[12.875rem]" />
|
||||
<Skeleton className="h-12 w-[12.875rem]" />
|
||||
<Skeleton className="h-12 w-[12.875rem]" />
|
||||
<Skeleton className="h-12 w-[12.875rem]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isError) {
|
||||
return <div className="w-fit space-y-2 px-4 pt-4">
|
||||
<ErrorCard className="w-[12.875rem]" httpError={{status: 500, statusText: "Internal Server Error", message: error?.detail || 'An error occurred'}} />
|
||||
</div>
|
||||
}
|
||||
|
||||
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: () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useGetV2GetBuilderItemCounts } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { CountResponse } from "@/app/api/__generated__/models/countResponse";
|
||||
|
||||
export const useBlockMenuSidebar = () => {
|
||||
const { defaultState, setDefaultState } = useBlockMenuContext();
|
||||
|
||||
const { data: blockCounts, isLoading, isError, error} = useGetV2GetBuilderItemCounts({
|
||||
query : {
|
||||
select : (x) =>{
|
||||
return x.data as CountResponse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
blockCounts,
|
||||
setDefaultState,
|
||||
defaultState,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
export type DefaultStateType =
|
||||
| "suggestion"
|
||||
| "all_blocks"
|
||||
| "input_blocks"
|
||||
| "action_blocks"
|
||||
| "output_blocks"
|
||||
| "integrations"
|
||||
| "marketplace_agents"
|
||||
| "my_agents";
|
||||
|
||||
|
||||
interface BlockMenuContextType {
|
||||
searchQuery: string;
|
||||
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
searchId: string | undefined;
|
||||
setSearchId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
defaultState: DefaultStateType;
|
||||
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
|
||||
}
|
||||
|
||||
export const BlockMenuContext = createContext<BlockMenuContextType>(
|
||||
{} as BlockMenuContextType,
|
||||
);
|
||||
|
||||
interface BlockMenuStateProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function BlockMenuStateProvider({
|
||||
children,
|
||||
}: BlockMenuStateProviderProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchId, setSearchId] = useState<string | undefined>(undefined);
|
||||
const [defaultState, setDefaultState] = useState<DefaultStateType>("suggestion");
|
||||
|
||||
return (
|
||||
<BlockMenuContext.Provider
|
||||
value={{
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
searchId,
|
||||
setSearchId,
|
||||
defaultState,
|
||||
setDefaultState,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BlockMenuContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBlockMenuContext(): BlockMenuContextType {
|
||||
const context = useContext(BlockMenuContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useBlockMenuContext must be used within a BlockMenuStateProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function ActionButtons({
|
||||
context,
|
||||
}: ActionButtonsProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-2 sm:flex-row">
|
||||
<div className="flex flex-col flex-wrap gap-3 pt-2 sm:flex-row">
|
||||
{onRetry && (
|
||||
<Button onClick={onRetry} variant="outline" size="small">
|
||||
<ArrowClockwise size={16} weight="bold" />
|
||||
|
||||
Reference in New Issue
Block a user