-
Notifications
You must be signed in to change notification settings - Fork 46.2k
feat(frontend/builder): add filters to blocks menu #11654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
e4a7856
bca033a
35e8e99
041f670
b2c905c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { useBlockMenuStore } from "@/app/(platform)/build/stores/blockMenuStore"; | ||
| import { FilterChip } from "../FilterChip"; | ||
| import { categories } from "./constants"; | ||
| import { FilterSheet } from "../FilterSheet/FilterSheet"; | ||
| import { GetV2BuilderSearchFilterAnyOfItem } from "@/app/api/__generated__/models/getV2BuilderSearchFilterAnyOfItem"; | ||
|
|
||
| export const BlockMenuFilters = () => { | ||
| const { | ||
| filters, | ||
| addFilter, | ||
| removeFilter, | ||
| categoryCounts, | ||
| creators, | ||
| addCreator, | ||
| removeCreator, | ||
| } = useBlockMenuStore(); | ||
|
|
||
| const handleFilterClick = (filter: GetV2BuilderSearchFilterAnyOfItem) => { | ||
| if (filters.includes(filter)) { | ||
| removeFilter(filter); | ||
| } else { | ||
| addFilter(filter); | ||
| } | ||
| }; | ||
|
|
||
| const handleCreatorClick = (creator: string) => { | ||
| if (creators.includes(creator)) { | ||
| removeCreator(creator); | ||
| } else { | ||
| addCreator(creator); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex flex-wrap gap-2"> | ||
| <FilterSheet categories={categories} /> | ||
| {creators.length > 0 && | ||
| creators.map((creator) => ( | ||
| <FilterChip | ||
| key={creator} | ||
| name={"Created by " + creator.slice(0, 10) + "..."} | ||
| selected={creators.includes(creator)} | ||
| onClick={() => handleCreatorClick(creator)} | ||
| /> | ||
| ))} | ||
| {categories.map((category) => ( | ||
| <FilterChip | ||
| key={category.key} | ||
| name={category.name} | ||
| selected={filters.includes(category.key)} | ||
| onClick={() => handleFilterClick(category.key)} | ||
| number={categoryCounts[category.key] ?? 0} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the proper way? Can't this just go to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yaa... sorry - in hurry I named it constants instead of types - will change it |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { GetV2BuilderSearchFilterAnyOfItem } from "@/app/api/__generated__/models/getV2BuilderSearchFilterAnyOfItem"; | ||
| import { CategoryKey } from "./types"; | ||
|
|
||
| export const categories: Array<{ key: CategoryKey; name: string }> = [ | ||
| { key: GetV2BuilderSearchFilterAnyOfItem.blocks, name: "Blocks" }, | ||
| { | ||
| key: GetV2BuilderSearchFilterAnyOfItem.integrations, | ||
| name: "Integrations", | ||
| }, | ||
| { | ||
| key: GetV2BuilderSearchFilterAnyOfItem.marketplace_agents, | ||
| name: "Marketplace agents", | ||
| }, | ||
| { key: GetV2BuilderSearchFilterAnyOfItem.my_agents, name: "My agents" }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { GetV2BuilderSearchFilterAnyOfItem } from "@/app/api/__generated__/models/getV2BuilderSearchFilterAnyOfItem"; | ||
|
|
||
| export type DefaultStateType = | ||
| | "suggestion" | ||
| | "all_blocks" | ||
| | "input_blocks" | ||
| | "action_blocks" | ||
| | "output_blocks" | ||
| | "integrations" | ||
| | "marketplace_agents" | ||
| | "my_agents"; | ||
|
|
||
| export type CategoryKey = GetV2BuilderSearchFilterAnyOfItem; | ||
|
|
||
| export interface Filters { | ||
| categories: { | ||
| blocks: boolean; | ||
| integrations: boolean; | ||
| marketplace_agents: boolean; | ||
| my_agents: boolean; | ||
| providers: boolean; | ||
| }; | ||
| createdBy: string[]; | ||
| } | ||
|
|
||
| export type CategoryCounts = Record<CategoryKey, number>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,111 +1,14 @@ | ||
| import { Text } from "@/components/atoms/Text/Text"; | ||
| import { useBlockMenuSearch } from "./useBlockMenuSearch"; | ||
| import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; | ||
| import { LoadingSpinner } from "@/components/__legacy__/ui/loading"; | ||
| import { SearchResponseItemsItem } from "@/app/api/__generated__/models/searchResponseItemsItem"; | ||
| import { MarketplaceAgentBlock } from "../MarketplaceAgentBlock"; | ||
| import { Block } from "../Block"; | ||
| import { UGCAgentBlock } from "../UGCAgentBlock"; | ||
| import { getSearchItemType } from "./helper"; | ||
| import { useBlockMenuStore } from "../../../../stores/blockMenuStore"; | ||
| import { blockMenuContainerStyle } from "../style"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { NoSearchResult } from "../NoSearchResult"; | ||
| import { BlockMenuFilters } from "../BlockMenuFilters/BlockMenuFilters"; | ||
| import { BlockMenuSearchContent } from "../BlockMenuSearchContent/BlockMenuSearchContent"; | ||
|
|
||
| export const BlockMenuSearch = () => { | ||
| const { | ||
| searchResults, | ||
| isFetchingNextPage, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| searchLoading, | ||
| handleAddLibraryAgent, | ||
| handleAddMarketplaceAgent, | ||
| addingLibraryAgentId, | ||
| addingMarketplaceAgentSlug, | ||
| } = useBlockMenuSearch(); | ||
| const { searchQuery } = useBlockMenuStore(); | ||
|
|
||
| if (searchLoading) { | ||
| return ( | ||
| <div | ||
| className={cn( | ||
| blockMenuContainerStyle, | ||
| "flex items-center justify-center", | ||
| )} | ||
| > | ||
| <LoadingSpinner className="size-13" /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (searchResults.length === 0) { | ||
| return <NoSearchResult />; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={blockMenuContainerStyle}> | ||
| <BlockMenuFilters /> | ||
| <Text variant="body-medium">Search results</Text> | ||
| <InfiniteScroll | ||
| isFetchingNextPage={isFetchingNextPage} | ||
| fetchNextPage={fetchNextPage} | ||
| hasNextPage={hasNextPage} | ||
| loader={<LoadingSpinner className="size-13" />} | ||
| className="space-y-2.5" | ||
| > | ||
| {searchResults.map((item: SearchResponseItemsItem, index: number) => { | ||
| const { type, data } = getSearchItemType(item); | ||
| // backend give support to these 3 types only [right now] - we need to give support to integration and ai agent types in follow up PRs | ||
| switch (type) { | ||
| case "store_agent": | ||
| return ( | ||
| <MarketplaceAgentBlock | ||
| key={index} | ||
| slug={data.slug} | ||
| highlightedText={searchQuery} | ||
| title={data.agent_name} | ||
| image_url={data.agent_image} | ||
| creator_name={data.creator} | ||
| number_of_runs={data.runs} | ||
| loading={addingMarketplaceAgentSlug === data.slug} | ||
| onClick={() => | ||
| handleAddMarketplaceAgent({ | ||
| creator_name: data.creator, | ||
| slug: data.slug, | ||
| }) | ||
| } | ||
| /> | ||
| ); | ||
| case "block": | ||
| return ( | ||
| <Block | ||
| key={index} | ||
| title={data.name} | ||
| highlightedText={searchQuery} | ||
| description={data.description} | ||
| blockData={data} | ||
| /> | ||
| ); | ||
|
|
||
| case "library_agent": | ||
| return ( | ||
| <UGCAgentBlock | ||
| key={index} | ||
| title={data.name} | ||
| highlightedText={searchQuery} | ||
| image_url={data.image_url} | ||
| version={data.graph_version} | ||
| edited_time={data.updated_at} | ||
| isLoading={addingLibraryAgentId === data.id} | ||
| onClick={() => handleAddLibraryAgent(data)} | ||
| /> | ||
| ); | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| })} | ||
| </InfiniteScroll> | ||
| <BlockMenuSearchContent /> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { SearchResponseItemsItem } from "@/app/api/__generated__/models/searchResponseItemsItem"; | ||
| import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; | ||
| import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; | ||
| import { getSearchItemType } from "./helper"; | ||
| import { MarketplaceAgentBlock } from "../MarketplaceAgentBlock"; | ||
| import { Block } from "../Block"; | ||
| import { UGCAgentBlock } from "../UGCAgentBlock"; | ||
| import { useBlockMenuSearchContent } from "./useBlockMenuSearchContent"; | ||
| import { useBlockMenuStore } from "@/app/(platform)/build/stores/blockMenuStore"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { blockMenuContainerStyle } from "../style"; | ||
| import { NoSearchResult } from "../NoSearchResult"; | ||
|
|
||
| export const BlockMenuSearchContent = () => { | ||
| const { | ||
| searchResults, | ||
| isFetchingNextPage, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| searchLoading, | ||
| handleAddLibraryAgent, | ||
| handleAddMarketplaceAgent, | ||
| addingLibraryAgentId, | ||
| addingMarketplaceAgentSlug, | ||
| } = useBlockMenuSearchContent(); | ||
|
|
||
| const { searchQuery } = useBlockMenuStore(); | ||
|
|
||
| if (searchLoading) { | ||
| return ( | ||
| <div | ||
| className={cn( | ||
| blockMenuContainerStyle, | ||
| "flex items-center justify-center", | ||
| )} | ||
| > | ||
| <LoadingSpinner className="size-13" /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (searchResults.length === 0) { | ||
| return <NoSearchResult />; | ||
| } | ||
|
|
||
| return ( | ||
| <InfiniteScroll | ||
| isFetchingNextPage={isFetchingNextPage} | ||
| fetchNextPage={fetchNextPage} | ||
| hasNextPage={hasNextPage} | ||
| loader={<LoadingSpinner className="size-13" />} | ||
| className="space-y-2.5" | ||
| > | ||
| {searchResults.map((item: SearchResponseItemsItem, index: number) => { | ||
| const { type, data } = getSearchItemType(item); | ||
| // backend give support to these 3 types only [right now] - we need to give support to integration and ai agent types in follow up PRs | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean? Is there something missing? From backend perspective block == integration and I'm unsure what you mean by "ai agent" as in AI suggestion?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In designs, search blocks come in two more types. I’ve attached an image of them. I’m only receiving these types of responses from the backend. /**
* Generated by orval v7.13.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { BlockInfo } from "./blockInfo";
import type { LibraryAgent } from "./libraryAgent";
import type { StoreAgent } from "./storeAgent";
export type SearchResponseItemsItem = BlockInfo | LibraryAgent | StoreAgent; |
||
| switch (type) { | ||
| case "store_agent": | ||
| return ( | ||
| <MarketplaceAgentBlock | ||
| key={index} | ||
| slug={data.slug} | ||
| highlightedText={searchQuery} | ||
| title={data.agent_name} | ||
| image_url={data.agent_image} | ||
| creator_name={data.creator} | ||
| number_of_runs={data.runs} | ||
| loading={addingMarketplaceAgentSlug === data.slug} | ||
| onClick={() => | ||
| handleAddMarketplaceAgent({ | ||
| creator_name: data.creator, | ||
| slug: data.slug, | ||
| }) | ||
| } | ||
| /> | ||
| ); | ||
| case "block": | ||
| return ( | ||
| <Block | ||
| key={index} | ||
| title={data.name} | ||
| highlightedText={searchQuery} | ||
| description={data.description} | ||
| blockData={data} | ||
| /> | ||
| ); | ||
|
|
||
| case "library_agent": | ||
| return ( | ||
| <UGCAgentBlock | ||
| key={index} | ||
| title={data.name} | ||
| highlightedText={searchQuery} | ||
| image_url={data.image_url} | ||
| version={data.graph_version} | ||
| edited_time={data.updated_at} | ||
| isLoading={addingLibraryAgentId === data.id} | ||
| onClick={() => handleAddLibraryAgent(data)} | ||
| /> | ||
| ); | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| })} | ||
| </InfiniteScroll> | ||
| ); | ||
| }; | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
useCallbackand for other functions as wellThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@0ubbe told me to skip
useCallbackunless there's a real performance issue. and these are just simple toggle functions with a few filter chips, so the overhead ofuseCallbackprobably isn't worth it here.