From 103a62c9dae80792674beaada75dd49e28372bba Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:32:21 +0530 Subject: [PATCH 01/23] feat(frontend/builder): add filters to blocks menu (#11654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ This PR adds filtering functionality to the new blocks menu, allowing users to filter search results by category and creator. **New Components:** - `BlockMenuFilters`: Main filter component displaying active filters and filter chips - `FilterSheet`: Slide-out panel for selecting filters with categories and creators - `BlockMenuSearchContent`: Refactored search results display component **Features Added:** - Filter by categories: Blocks, Integrations, Marketplace agents, My agents - Filter by creator: Shows all available creators from search results - Category counts: Display number of results per category - Interactive filter chips with animations (using framer-motion) - Hover states showing result counts on filter chips - "All filters" sheet with apply/clear functionality **State Management:** - Extended `blockMenuStore` with filter state management - Added `filters`, `creators`, `creators_list`, and `categoryCounts` to store - Integrated filters with search API (`filter` and `by_creator` parameters) **Refactoring:** - Moved search logic from `BlockMenuSearch` to `BlockMenuSearchContent` - Renamed `useBlockMenuSearch` to `useBlockMenuSearchContent` - Moved helper functions to `BlockMenuSearchContent` directory **API Changes:** - Updated `custom-mutator.ts` to properly handle query parameter encoding ### Checklist 📋 #### For code changes: - [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] Search for blocks and verify filter chips appear - [x] Click "All filters" and verify filter sheet opens with categories - [x] Select/deselect category filters and verify results update accordingly - [x] Filter by creator and verify only blocks from that creator show - [x] Clear all filters and verify reset to default state - [x] Verify filter counts display correctly - [x] Test filter chip hover animations --- .../BlockMenuFilters/BlockMenuFilters.tsx | 57 +++++++ .../BlockMenuFilters/constants.ts | 15 ++ .../NewBlockMenu/BlockMenuFilters/types.ts | 26 +++ .../BlockMenuSearch/BlockMenuSearch.tsx | 105 +----------- .../BlockMenuSearchContent.tsx | 108 ++++++++++++ .../helper.ts | 0 .../useBlockMenuSearchContent.tsx} | 36 +++- .../NewBlockMenu/FilterChip.tsx | 76 +++++---- .../NewBlockMenu/FilterSheet/FilterSheet.tsx | 156 ++++++++++++++++++ .../NewBlockMenu/FilterSheet/constant.ts | 1 + .../FilterSheet/useFilterSheet.ts | 100 +++++++++++ .../(platform)/build/stores/blockMenuStore.ts | 51 ++++++ .../src/app/api/mutators/custom-mutator.ts | 13 +- 13 files changed, 601 insertions(+), 143 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/BlockMenuFilters.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/constants.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/types.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/BlockMenuSearchContent.tsx rename autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/{BlockMenuSearch => BlockMenuSearchContent}/helper.ts (100%) rename autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/{BlockMenuSearch/useBlockMenuSearch.ts => BlockMenuSearchContent/useBlockMenuSearchContent.tsx} (83%) create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/FilterSheet.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/constant.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/useFilterSheet.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/BlockMenuFilters.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/BlockMenuFilters.tsx new file mode 100644 index 0000000000..ebcea9eee6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/BlockMenuFilters.tsx @@ -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 ( +
+ + {creators.length > 0 && + creators.map((creator) => ( + handleCreatorClick(creator)} + /> + ))} + {categories.map((category) => ( + handleFilterClick(category.key)} + number={categoryCounts[category.key] ?? 0} + /> + ))} +
+ ); +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/constants.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/constants.ts new file mode 100644 index 0000000000..b438aae91b --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/constants.ts @@ -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" }, +]; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/types.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/types.ts new file mode 100644 index 0000000000..8fec9ef64d --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuFilters/types.ts @@ -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; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx index de339431e8..26723eebcc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx @@ -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 ( -
- -
- ); - } - - if (searchResults.length === 0) { - return ; - } - return (
+ Search results - } - 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 ( - - handleAddMarketplaceAgent({ - creator_name: data.creator, - slug: data.slug, - }) - } - /> - ); - case "block": - return ( - - ); - - case "library_agent": - return ( - handleAddLibraryAgent(data)} - /> - ); - - default: - return null; - } - })} - +
); }; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/BlockMenuSearchContent.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/BlockMenuSearchContent.tsx new file mode 100644 index 0000000000..7229c44ed7 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/BlockMenuSearchContent.tsx @@ -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 ( +
+ +
+ ); + } + + if (searchResults.length === 0) { + return ; + } + + return ( + } + 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 ( + + handleAddMarketplaceAgent({ + creator_name: data.creator, + slug: data.slug, + }) + } + /> + ); + case "block": + return ( + + ); + + case "library_agent": + return ( + handleAddLibraryAgent(data)} + /> + ); + + default: + return null; + } + })} + + ); +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/helper.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/helper.ts similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/helper.ts rename to autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/helper.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/useBlockMenuSearch.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/useBlockMenuSearchContent.tsx similarity index 83% rename from autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/useBlockMenuSearch.ts rename to autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/useBlockMenuSearchContent.tsx index beff80a984..9da9cb4cbc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/useBlockMenuSearch.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/useBlockMenuSearchContent.tsx @@ -23,9 +23,19 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { getQueryClient } from "@/lib/react-query/queryClient"; import { useToast } from "@/components/molecules/Toast/use-toast"; import * as Sentry from "@sentry/nextjs"; +import { GetV2BuilderSearchFilterAnyOfItem } from "@/app/api/__generated__/models/getV2BuilderSearchFilterAnyOfItem"; + +export const useBlockMenuSearchContent = () => { + const { + searchQuery, + searchId, + setSearchId, + filters, + setCreatorsList, + creators, + setCategoryCounts, + } = useBlockMenuStore(); -export const useBlockMenuSearch = () => { - const { searchQuery, searchId, setSearchId } = useBlockMenuStore(); const { toast } = useToast(); const { addAgentToBuilder, addLibraryAgentToBuilder } = useAddAgentToBuilder(); @@ -57,6 +67,8 @@ export const useBlockMenuSearch = () => { page_size: 8, search_query: searchQuery, search_id: searchId, + filter: filters.length > 0 ? filters : undefined, + by_creator: creators.length > 0 ? creators : undefined, }, { query: { getNextPageParam: getPaginationNextPageNumber }, @@ -98,6 +110,26 @@ export const useBlockMenuSearch = () => { } }, [searchQueryData, searchId, setSearchId]); + // from all the results, we need to get all the unique creators + useEffect(() => { + if (!searchQueryData?.pages?.length) { + return; + } + const latestData = okData(searchQueryData.pages.at(-1)); + setCategoryCounts( + (latestData?.total_items as Record< + GetV2BuilderSearchFilterAnyOfItem, + number + >) || { + blocks: 0, + integrations: 0, + marketplace_agents: 0, + my_agents: 0, + }, + ); + setCreatorsList(latestData?.items || []); + }, [searchQueryData]); + useEffect(() => { if (searchId && !searchQuery) { resetSearchSession(); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterChip.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterChip.tsx index 69931958b3..23197ab612 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterChip.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterChip.tsx @@ -1,7 +1,9 @@ import { Button } from "@/components/__legacy__/ui/button"; import { cn } from "@/lib/utils"; -import { X } from "lucide-react"; -import React, { ButtonHTMLAttributes } from "react"; +import { XIcon } from "@phosphor-icons/react"; +import { AnimatePresence, motion } from "framer-motion"; + +import React, { ButtonHTMLAttributes, useState } from "react"; interface Props extends ButtonHTMLAttributes { selected?: boolean; @@ -16,39 +18,51 @@ export const FilterChip: React.FC = ({ className, ...rest }) => { + const [isHovered, setIsHovered] = useState(false); return ( - + > + {name} + + {selected && !isHovered && ( + + + + )} + {number !== undefined && isHovered && ( + + {number > 100 ? "100+" : number} + + )} + + ); }; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/FilterSheet.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/FilterSheet.tsx new file mode 100644 index 0000000000..dc7c428245 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/FilterSheet.tsx @@ -0,0 +1,156 @@ +import { FilterChip } from "../FilterChip"; +import { cn } from "@/lib/utils"; +import { CategoryKey } from "../BlockMenuFilters/types"; +import { AnimatePresence, motion } from "framer-motion"; +import { XIcon } from "@phosphor-icons/react"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { Separator } from "@/components/__legacy__/ui/separator"; +import { Checkbox } from "@/components/__legacy__/ui/checkbox"; +import { useFilterSheet } from "./useFilterSheet"; +import { INITIAL_CREATORS_TO_SHOW } from "./constant"; + +export function FilterSheet({ + categories, +}: { + categories: Array<{ key: CategoryKey; name: string }>; +}) { + const { + isOpen, + localCategories, + localCreators, + displayedCreatorsCount, + handleLocalCategoryChange, + handleToggleShowMoreCreators, + handleLocalCreatorChange, + handleClearFilters, + handleCloseButton, + handleApplyFilters, + hasLocalActiveFilters, + visibleCreators, + creators, + handleOpenFilters, + hasActiveFilters, + } = useFilterSheet(); + + return ( +
+ + + + {isOpen && ( + + {/* Top section */} +
+ Filters + +
+ + + + {/* Category section */} +
+ Categories +
+ {categories.map((category) => ( +
+ + handleLocalCategoryChange(category.key) + } + className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white" + /> + +
+ ))} +
+
+ + {/* Created by section */} +
+

+ Created by +

+
+ {visibleCreators.map((creator, i) => ( +
+ handleLocalCreatorChange(creator)} + className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white" + /> + +
+ ))} +
+ {creators.length > INITIAL_CREATORS_TO_SHOW && ( + + )} +
+ + {/* Footer section */} +
+ + + +
+
+ )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/constant.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/constant.ts new file mode 100644 index 0000000000..8e05dc1037 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/constant.ts @@ -0,0 +1 @@ +export const INITIAL_CREATORS_TO_SHOW = 5; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/useFilterSheet.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/useFilterSheet.ts new file mode 100644 index 0000000000..200671f4e7 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/FilterSheet/useFilterSheet.ts @@ -0,0 +1,100 @@ +import { useBlockMenuStore } from "@/app/(platform)/build/stores/blockMenuStore"; +import { useState } from "react"; +import { INITIAL_CREATORS_TO_SHOW } from "./constant"; +import { GetV2BuilderSearchFilterAnyOfItem } from "@/app/api/__generated__/models/getV2BuilderSearchFilterAnyOfItem"; + +export const useFilterSheet = () => { + const { filters, creators_list, creators, setFilters, setCreators } = + useBlockMenuStore(); + + const [isOpen, setIsOpen] = useState(false); + const [localCategories, setLocalCategories] = + useState(filters); + const [localCreators, setLocalCreators] = useState(creators); + const [displayedCreatorsCount, setDisplayedCreatorsCount] = useState( + INITIAL_CREATORS_TO_SHOW, + ); + + const handleLocalCategoryChange = ( + category: GetV2BuilderSearchFilterAnyOfItem, + ) => { + setLocalCategories((prev) => { + if (prev.includes(category)) { + return prev.filter((c) => c !== category); + } + return [...prev, category]; + }); + }; + + const hasActiveFilters = () => { + return filters.length > 0 || creators.length > 0; + }; + + const handleToggleShowMoreCreators = () => { + if (displayedCreatorsCount < creators.length) { + setDisplayedCreatorsCount(creators.length); + } else { + setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW); + } + }; + + const handleLocalCreatorChange = (creator: string) => { + setLocalCreators((prev) => { + if (prev.includes(creator)) { + return prev.filter((c) => c !== creator); + } + return [...prev, creator]; + }); + }; + + const handleClearFilters = () => { + setLocalCategories([]); + setLocalCreators([]); + setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW); + }; + + const handleCloseButton = () => { + setIsOpen(false); + setLocalCategories(filters); + setLocalCreators(creators); + setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW); + }; + + const handleApplyFilters = () => { + setFilters(localCategories); + setCreators(localCreators); + setIsOpen(false); + }; + + const handleOpenFilters = () => { + setIsOpen(true); + setLocalCategories(filters); + setLocalCreators(creators); + }; + + const hasLocalActiveFilters = () => { + return localCategories.length > 0 || localCreators.length > 0; + }; + + const visibleCreators = creators_list.slice(0, displayedCreatorsCount); + + return { + creators, + isOpen, + setIsOpen, + localCategories, + localCreators, + displayedCreatorsCount, + setDisplayedCreatorsCount, + handleLocalCategoryChange, + handleToggleShowMoreCreators, + handleLocalCreatorChange, + handleClearFilters, + handleCloseButton, + handleOpenFilters, + handleApplyFilters, + hasLocalActiveFilters, + visibleCreators, + hasActiveFilters, + }; +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/blockMenuStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/blockMenuStore.ts index ea50a03979..31b9eda338 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/stores/blockMenuStore.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/blockMenuStore.ts @@ -1,12 +1,30 @@ import { create } from "zustand"; import { DefaultStateType } from "../components/NewControlPanel/NewBlockMenu/types"; +import { SearchResponseItemsItem } from "@/app/api/__generated__/models/searchResponseItemsItem"; +import { getSearchItemType } from "../components/NewControlPanel/NewBlockMenu/BlockMenuSearchContent/helper"; +import { StoreAgent } from "@/app/api/__generated__/models/storeAgent"; +import { GetV2BuilderSearchFilterAnyOfItem } from "@/app/api/__generated__/models/getV2BuilderSearchFilterAnyOfItem"; type BlockMenuStore = { searchQuery: string; searchId: string | undefined; defaultState: DefaultStateType; integration: string | undefined; + filters: GetV2BuilderSearchFilterAnyOfItem[]; + creators: string[]; + creators_list: string[]; + categoryCounts: Record; + setCategoryCounts: ( + counts: Record, + ) => void; + setCreatorsList: (searchData: SearchResponseItemsItem[]) => void; + addCreator: (creator: string) => void; + setCreators: (creators: string[]) => void; + removeCreator: (creator: string) => void; + addFilter: (filter: GetV2BuilderSearchFilterAnyOfItem) => void; + setFilters: (filters: GetV2BuilderSearchFilterAnyOfItem[]) => void; + removeFilter: (filter: GetV2BuilderSearchFilterAnyOfItem) => void; setSearchQuery: (query: string) => void; setSearchId: (id: string | undefined) => void; setDefaultState: (state: DefaultStateType) => void; @@ -19,11 +37,44 @@ export const useBlockMenuStore = create((set) => ({ searchId: undefined, defaultState: DefaultStateType.SUGGESTION, integration: undefined, + filters: [], + creators: [], // creator filters that are applied to the search results + creators_list: [], // all creators that are available to filter by + categoryCounts: { + blocks: 0, + integrations: 0, + marketplace_agents: 0, + my_agents: 0, + }, + setCategoryCounts: (counts) => set({ categoryCounts: counts }), + setCreatorsList: (searchData) => { + const marketplaceAgents = searchData.filter((item) => { + return getSearchItemType(item).type === "store_agent"; + }) as StoreAgent[]; + + const newCreators = marketplaceAgents.map((agent) => agent.creator); + + set((state) => ({ + creators_list: Array.from( + new Set([...state.creators_list, ...newCreators]), + ), + })); + }, + setCreators: (creators) => set({ creators }), + setFilters: (filters) => set({ filters }), setSearchQuery: (query) => set({ searchQuery: query }), setSearchId: (id) => set({ searchId: id }), setDefaultState: (state) => set({ defaultState: state }), setIntegration: (integration) => set({ integration }), + addFilter: (filter) => + set((state) => ({ filters: [...state.filters, filter] })), + removeFilter: (filter) => + set((state) => ({ filters: state.filters.filter((f) => f !== filter) })), + addCreator: (creator) => + set((state) => ({ creators: [...state.creators, creator] })), + removeCreator: (creator) => + set((state) => ({ creators: state.creators.filter((c) => c !== creator) })), reset: () => set({ searchQuery: "", diff --git a/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts index 315b68ab87..4578ac03fe 100644 --- a/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts +++ b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts @@ -41,11 +41,9 @@ export const customMutator = async < T extends { data: any; status: number; headers: Headers }, >( url: string, - options: RequestInit & { - params?: any; - } = {}, + options: RequestInit, ): Promise => { - const { params, ...requestOptions } = options; + const requestOptions = options; const method = (requestOptions.method || "GET") as | "GET" | "POST" @@ -87,14 +85,11 @@ export const customMutator = async < headers["Content-Type"] = "application/json"; } - const queryString = params - ? "?" + new URLSearchParams(params).toString() - : ""; - const baseUrl = getBaseUrl(); // The caching in React Query in our system depends on the url, so the base_url could be different for the server and client sides. - const fullUrl = `${baseUrl}${url}${queryString}`; + // here url also contains encoded query params + const fullUrl = `${baseUrl}${url}`; if (environment.isServerSide()) { try { From 5e2146dd7666b0cfe44597383ede059d92e61d34 Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:17:52 +0530 Subject: [PATCH 02/23] feat(frontend): add CustomSchemaField wrapper for dynamic form field routing (#11722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ This PR introduces automatic UI schema generation for custom form fields, eliminating manual field mapping. #### 1. **generateUiSchemaForCustomFields Utility** (`generate-ui-schema.ts`) - New File - Auto-generates `ui:field` settings for custom fields - Detects custom fields using `findCustomFieldId()` matcher - Handles nested objects and array items recursively - Merges with existing UI schema without overwriting #### 2. **FormRenderer Integration** (`FormRenderer.tsx`) - Imports and uses `generateUiSchemaForCustomFields` - Creates merged UI schema with `useMemo` - Passes merged schema to Form component - Enables automatic custom field detection #### 3. **Preprocessor Cleanup** (`input-schema-pre-processor.ts`) - Removed manual `$id` assignment for custom fields - Removed unused `findCustomFieldId` import - Simplified to focus only on type validation ### Why these changes? - Custom fields now auto-detect without manual `ui:field` configuration - Uses standard RJSF approach (UI schema) for field routing - Centralized custom field detection logic improves maintainability ### Checklist 📋 #### For code changes: - [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] Verify custom fields render correctly when present in schema - [x] Verify standard fields continue to render with default SchemaField - [x] Verify multiple instances of same custom field type have unique IDs - [x] Test form submission with custom fields ## Summary by CodeRabbit * **Bug Fixes** * Improved custom field rendering in forms by optimizing the UI schema generation process. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../renderers/InputRenderer/FormRenderer.tsx | 8 ++- .../InputRenderer/utils/generate-ui-schema.ts | 71 +++++++++++++++++++ .../utils/input-schema-pre-processor.ts | 7 -- 3 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/generate-ui-schema.ts diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx index 6bc785e212..f784b64516 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx @@ -4,6 +4,7 @@ import { useMemo } from "react"; import { customValidator } from "./utils/custom-validator"; import Form from "./registry"; import { ExtendedFormContextType } from "./types"; +import { generateUiSchemaForCustomFields } from "./utils/generate-ui-schema"; type FormRendererProps = { jsonSchema: RJSFSchema; @@ -24,6 +25,11 @@ export const FormRenderer = ({ return preprocessInputSchema(jsonSchema); }, [jsonSchema]); + // Merge custom field ui:field settings with existing uiSchema + const mergedUiSchema = useMemo(() => { + return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema); + }, [preprocessedSchema, uiSchema]); + return (
diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/generate-ui-schema.ts b/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/generate-ui-schema.ts new file mode 100644 index 0000000000..4a2f4fc44a --- /dev/null +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/generate-ui-schema.ts @@ -0,0 +1,71 @@ +import { RJSFSchema, UiSchema } from "@rjsf/utils"; +import { findCustomFieldId } from "../custom/custom-registry"; + +/** + * Generates uiSchema with ui:field settings for custom fields based on schema matchers. + * This is the standard RJSF way to route fields to custom components. + */ +export function generateUiSchemaForCustomFields( + schema: RJSFSchema, + existingUiSchema: UiSchema = {}, +): UiSchema { + const uiSchema: UiSchema = { ...existingUiSchema }; + + if (schema.properties) { + for (const [key, propSchema] of Object.entries(schema.properties)) { + if (propSchema && typeof propSchema === "object") { + const customFieldId = findCustomFieldId(propSchema); + + if (customFieldId) { + uiSchema[key] = { + ...(uiSchema[key] as object), + "ui:field": customFieldId, + }; + } + + if ( + propSchema.type === "object" && + propSchema.properties && + typeof propSchema.properties === "object" + ) { + const nestedUiSchema = generateUiSchemaForCustomFields( + propSchema as RJSFSchema, + (uiSchema[key] as UiSchema) || {}, + ); + uiSchema[key] = { + ...(uiSchema[key] as object), + ...nestedUiSchema, + }; + } + + if (propSchema.type === "array" && propSchema.items) { + const itemsSchema = propSchema.items as RJSFSchema; + if (itemsSchema && typeof itemsSchema === "object") { + const itemsCustomFieldId = findCustomFieldId(itemsSchema); + if (itemsCustomFieldId) { + uiSchema[key] = { + ...(uiSchema[key] as object), + items: { + "ui:field": itemsCustomFieldId, + }, + }; + } else if (itemsSchema.properties) { + const itemsUiSchema = generateUiSchemaForCustomFields( + itemsSchema, + ((uiSchema[key] as UiSchema)?.items as UiSchema) || {}, + ); + if (Object.keys(itemsUiSchema).length > 0) { + uiSchema[key] = { + ...(uiSchema[key] as object), + items: itemsUiSchema, + }; + } + } + } + } + } + } + } + + return uiSchema; +} diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/input-schema-pre-processor.ts b/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/input-schema-pre-processor.ts index dad95251ed..9d1aa2c60c 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/input-schema-pre-processor.ts +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/utils/input-schema-pre-processor.ts @@ -1,5 +1,4 @@ import { RJSFSchema } from "@rjsf/utils"; -import { findCustomFieldId } from "../custom/custom-registry"; /** * Pre-processes the input schema to ensure all properties have a type defined. @@ -21,12 +20,6 @@ export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema { if (property && typeof property === "object") { const processedProperty = { ...property }; - // adding $id for custom field - const customFieldId = findCustomFieldId(processedProperty); - if (customFieldId) { - processedProperty.$id = customFieldId; - } - // Only add type if no type is defined AND no anyOf/oneOf/allOf is present if ( !processedProperty.type && From b0855e8cf212a14873cc775e7469d326e3120010 Mon Sep 17 00:00:00 2001 From: Ubbe Date: Thu, 8 Jan 2026 17:35:49 +0700 Subject: [PATCH 03/23] feat(frontend): context menu right click new builder (#11703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ Screenshot 2026-01-06 at 17 53 26 Screenshot 2026-01-06 at 17 53 29 On the **New Builder**: - right-click on a node menu make it show the context menu - use the same menu for right-click and when clicking on `...` ## Checklist 📋 ### For code changes: - [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] Run locally and test the above ## Summary by CodeRabbit * **New Features** * Added a custom right-click context menu for nodes with Copy, Open agent (when available), and Delete actions; browser default menu is suppressed while preserving zoom/drag/wiring. * Introduced reusable SecondaryMenu primitives for context and dropdown menus. * **Documentation** * Added Storybook examples demonstrating the context menu and dropdown menu usage. * **Style** * Updated menu styling and icons with improved consistency and dark-mode support. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../build/components/FlowEditor/Flow/Flow.tsx | 3 + .../nodes/CustomNode/CustomNode.tsx | 48 ++++---- .../CustomNode/components/NodeContextMenu.tsx | 89 ++++++++------- .../CustomNode/components/NodeHeader.tsx | 23 ++-- .../components/NodeRightClickMenu.tsx | 104 ++++++++++++++++++ .../SecondaryMenu/SecondaryMenu.stories.tsx | 99 +++++++++++++++++ .../molecules/SecondaryMenu/SecondaryMenu.tsx | 103 +++++++++++++++++ 7 files changed, 396 insertions(+), 73 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeRightClickMenu.tsx create mode 100644 autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.stories.tsx create mode 100644 autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/Flow.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/Flow.tsx index faaebb6b35..29fd984b1d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/Flow.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/Flow.tsx @@ -97,6 +97,9 @@ export const Flow = () => { onConnect={onConnect} onEdgesChange={onEdgesChange} onNodeDragStop={onNodeDragStop} + onNodeContextMenu={(event) => { + event.preventDefault(); + }} maxZoom={2} minZoom={0.1} onDragOver={onDragOver} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx index 3523079b71..99a5b9f0e5 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx @@ -1,24 +1,25 @@ -import React from "react"; -import { Node as XYNode, NodeProps } from "@xyflow/react"; -import { RJSFSchema } from "@rjsf/utils"; -import { BlockUIType } from "../../../types"; -import { StickyNoteBlock } from "./components/StickyNoteBlock"; -import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem"; -import { BlockCost } from "@/app/api/__generated__/models/blockCost"; import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus"; +import { BlockCost } from "@/app/api/__generated__/models/blockCost"; +import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem"; import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult"; -import { NodeContainer } from "./components/NodeContainer"; -import { NodeHeader } from "./components/NodeHeader"; -import { FormCreator } from "../FormCreator"; -import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor"; -import { OutputHandler } from "../OutputHandler"; -import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle"; -import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput"; -import { NodeExecutionBadge } from "./components/NodeExecutionBadge"; -import { cn } from "@/lib/utils"; -import { WebhookDisclaimer } from "./components/WebhookDisclaimer"; -import { AyrshareConnectButton } from "./components/AyrshareConnectButton"; import { NodeModelMetadata } from "@/app/api/__generated__/models/nodeModelMetadata"; +import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor"; +import { cn } from "@/lib/utils"; +import { RJSFSchema } from "@rjsf/utils"; +import { NodeProps, Node as XYNode } from "@xyflow/react"; +import React from "react"; +import { BlockUIType } from "../../../types"; +import { FormCreator } from "../FormCreator"; +import { OutputHandler } from "../OutputHandler"; +import { AyrshareConnectButton } from "./components/AyrshareConnectButton"; +import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle"; +import { NodeContainer } from "./components/NodeContainer"; +import { NodeExecutionBadge } from "./components/NodeExecutionBadge"; +import { NodeHeader } from "./components/NodeHeader"; +import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput"; +import { NodeRightClickMenu } from "./components/NodeRightClickMenu"; +import { StickyNoteBlock } from "./components/StickyNoteBlock"; +import { WebhookDisclaimer } from "./components/WebhookDisclaimer"; export type CustomNodeData = { hardcodedValues: { @@ -88,7 +89,7 @@ export const CustomNode: React.FC> = React.memo( // Currently all blockTypes design are similar - that's why i am using the same component for all of them // If in future - if we need some drastic change in some blockTypes design - we can create separate components for them - return ( + const node = (
@@ -117,6 +118,15 @@ export const CustomNode: React.FC> = React.memo( ); + + return ( + + {node} + + ); }, ); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContextMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContextMenu.tsx index 6e482122f6..1a0e23fead 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContextMenu.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeContextMenu.tsx @@ -1,26 +1,31 @@ -import { Separator } from "@/components/__legacy__/ui/separator"; +import { useCopyPasteStore } from "@/app/(platform)/build/stores/copyPasteStore"; +import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore"; import { DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "@/components/molecules/DropdownMenu/DropdownMenu"; -import { DotsThreeOutlineVerticalIcon } from "@phosphor-icons/react"; -import { Copy, Trash2, ExternalLink } from "lucide-react"; -import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore"; -import { useCopyPasteStore } from "@/app/(platform)/build/stores/copyPasteStore"; +import { + SecondaryDropdownMenuContent, + SecondaryDropdownMenuItem, + SecondaryDropdownMenuSeparator, +} from "@/components/molecules/SecondaryMenu/SecondaryMenu"; +import { + ArrowSquareOutIcon, + CopyIcon, + DotsThreeOutlineVerticalIcon, + TrashIcon, +} from "@phosphor-icons/react"; import { useReactFlow } from "@xyflow/react"; -export const NodeContextMenu = ({ - nodeId, - subGraphID, -}: { +type Props = { nodeId: string; subGraphID?: string; -}) => { +}; + +export const NodeContextMenu = ({ nodeId, subGraphID }: Props) => { const { deleteElements } = useReactFlow(); - const handleCopy = () => { + function handleCopy() { useNodeStore.setState((state) => ({ nodes: state.nodes.map((node) => ({ ...node, @@ -30,47 +35,47 @@ export const NodeContextMenu = ({ useCopyPasteStore.getState().copySelectedNodes(); useCopyPasteStore.getState().pasteNodes(); - }; + } - const handleDelete = () => { + function handleDelete() { deleteElements({ nodes: [{ id: nodeId }] }); - }; + } return ( - - - - Copy Node - + + + + Copy + + {subGraphID && ( - window.open(`/build?flowID=${subGraphID}`)} - className="hover:rounded-xlarge" - > - - Open Agent - + <> + window.open(`/build?flowID=${subGraphID}`)} + > + + Open agent + + + )} - - - - - Delete - - + + + Delete + + ); }; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeHeader.tsx index 4dadccef2b..5943986d30 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeHeader.tsx @@ -1,25 +1,24 @@ -import { Text } from "@/components/atoms/Text/Text"; -import { beautifyString, cn } from "@/lib/utils"; -import { NodeCost } from "./NodeCost"; -import { NodeBadges } from "./NodeBadges"; -import { NodeContextMenu } from "./NodeContextMenu"; -import { CustomNodeData } from "../CustomNode"; import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore"; -import { useState } from "react"; +import { Text } from "@/components/atoms/Text/Text"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/atoms/Tooltip/BaseTooltip"; +import { beautifyString, cn } from "@/lib/utils"; +import { useState } from "react"; +import { CustomNodeData } from "../CustomNode"; +import { NodeBadges } from "./NodeBadges"; +import { NodeContextMenu } from "./NodeContextMenu"; +import { NodeCost } from "./NodeCost"; -export const NodeHeader = ({ - data, - nodeId, -}: { +type Props = { data: CustomNodeData; nodeId: string; -}) => { +}; + +export const NodeHeader = ({ data, nodeId }: Props) => { const updateNodeData = useNodeStore((state) => state.updateNodeData); const title = (data.metadata?.customized_name as string) || data.title; const [isEditingTitle, setIsEditingTitle] = useState(false); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeRightClickMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeRightClickMenu.tsx new file mode 100644 index 0000000000..a56e42544f --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/components/NodeRightClickMenu.tsx @@ -0,0 +1,104 @@ +import { useCopyPasteStore } from "@/app/(platform)/build/stores/copyPasteStore"; +import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore"; +import { + SecondaryMenuContent, + SecondaryMenuItem, + SecondaryMenuSeparator, +} from "@/components/molecules/SecondaryMenu/SecondaryMenu"; +import { ArrowSquareOutIcon, CopyIcon, TrashIcon } from "@phosphor-icons/react"; +import * as ContextMenu from "@radix-ui/react-context-menu"; +import { useReactFlow } from "@xyflow/react"; +import { useEffect, useRef } from "react"; +import { CustomNode } from "../CustomNode"; + +type Props = { + nodeId: string; + subGraphID?: string; + children: React.ReactNode; +}; + +const DOUBLE_CLICK_TIMEOUT = 300; + +export function NodeRightClickMenu({ nodeId, subGraphID, children }: Props) { + const { deleteElements } = useReactFlow(); + const lastRightClickTime = useRef(0); + const containerRef = useRef(null); + + function copyNode() { + useNodeStore.setState((state) => ({ + nodes: state.nodes.map((node) => ({ + ...node, + selected: node.id === nodeId, + })), + })); + + useCopyPasteStore.getState().copySelectedNodes(); + useCopyPasteStore.getState().pasteNodes(); + } + + function deleteNode() { + deleteElements({ nodes: [{ id: nodeId }] }); + } + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + function handleContextMenu(e: MouseEvent) { + const now = Date.now(); + const timeSinceLastClick = now - lastRightClickTime.current; + + if (timeSinceLastClick < DOUBLE_CLICK_TIMEOUT) { + e.stopImmediatePropagation(); + lastRightClickTime.current = 0; + return; + } + + lastRightClickTime.current = now; + } + + container.addEventListener("contextmenu", handleContextMenu, true); + + return () => { + container.removeEventListener("contextmenu", handleContextMenu, true); + }; + }, []); + + return ( + + +
{children}
+
+ + + + Copy + + + + {subGraphID && ( + <> + window.open(`/build?flowID=${subGraphID}`)} + > + + Open agent + + + + )} + + + + Delete + + +
+ ); +} diff --git a/autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.stories.tsx b/autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.stories.tsx new file mode 100644 index 0000000000..907a552056 --- /dev/null +++ b/autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.stories.tsx @@ -0,0 +1,99 @@ +import { Button } from "@/components/atoms/Button/Button"; +import { + DropdownMenu, + DropdownMenuTrigger, +} from "@/components/molecules/DropdownMenu/DropdownMenu"; +import { + ArrowSquareOutIcon, + CopyIcon, + DotsThreeOutlineVerticalIcon, + TrashIcon, +} from "@phosphor-icons/react"; +import * as ContextMenu from "@radix-ui/react-context-menu"; +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { + SecondaryDropdownMenuContent, + SecondaryDropdownMenuItem, + SecondaryDropdownMenuSeparator, + SecondaryMenuContent, + SecondaryMenuItem, + SecondaryMenuSeparator, +} from "./SecondaryMenu"; + +const meta: Meta = { + title: "Molecules/SecondaryMenu", + component: SecondaryMenuContent, +}; + +export default meta; +type Story = StoryObj; + +export const ContextMenuExample: Story = { + render: () => ( +
+ + +
+ Right-click me +
+
+ + alert("Copy")}> + + Copy + + alert("Open agent")}> + + Open agent + + + alert("Delete")} + > + + Delete + + +
+
+ ), +}; + +export const DropdownMenuExample: Story = { + render: () => ( +
+ + + + + + alert("Copy")}> + + Copy + + alert("Open agent")}> + + Open agent + + + alert("Delete")} + > + + Delete + + + +
+ ), +}; diff --git a/autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.tsx b/autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.tsx new file mode 100644 index 0000000000..fd445491e7 --- /dev/null +++ b/autogpt_platform/frontend/src/components/molecules/SecondaryMenu/SecondaryMenu.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import * as ContextMenu from "@radix-ui/react-context-menu"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import React from "react"; + +const secondaryMenuContentClassName = + "z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800"; + +const secondaryMenuItemClassName = + "flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"; + +const secondaryMenuSeparatorClassName = + "my-1 h-px bg-gray-300 dark:bg-gray-600"; + +export const SecondaryMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SecondaryMenuContent.displayName = "SecondaryMenuContent"; + +export const SecondaryMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + variant?: "default" | "destructive"; + } +>(({ className, variant = "default", ...props }, ref) => ( + +)); +SecondaryMenuItem.displayName = "SecondaryMenuItem"; + +export const SecondaryMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SecondaryMenuSeparator.displayName = "SecondaryMenuSeparator"; + +export const SecondaryDropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SecondaryDropdownMenuContent.displayName = "SecondaryDropdownMenuContent"; + +export const SecondaryDropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + variant?: "default" | "destructive"; + } +>(({ className, variant = "default", ...props }, ref) => ( + +)); +SecondaryDropdownMenuItem.displayName = "SecondaryDropdownMenuItem"; + +export const SecondaryDropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SecondaryDropdownMenuSeparator.displayName = "SecondaryDropdownMenuSeparator"; From fc25e008b39bc94237b42ce5a28657208ba1ef1d Mon Sep 17 00:00:00 2001 From: Ubbe Date: Thu, 8 Jan 2026 18:28:27 +0700 Subject: [PATCH 04/23] feat(frontend): update library agent cards to use DS (#11720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ Screenshot 2026-01-07 at 16 11 04 - Update the agent library cards to new designs - Update page to use Design System components - Allow to edit/delete/duplicate agents on the library list page - Add missing actions on library agent detail page ## Checklist 📋 ### For code changes: - [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] Run locally and test the above ## Summary by CodeRabbit * **New Features** * Marketplace info shown on agent cards and improved favoriting with optimistic UI and feedback. * Delete agent and delete schedule flows with confirmation dialogs. * **Refactor** * New composable form system, modernized upload dialog, streamlined search bar, and multiple library components converted to named exports with layout tweaks. * New agent card menu and favorite button UI. * **Chores** * Removed notification UI and dropped a drag-drop dependency. * **Tests** * Increased timeouts and stabilized upload/pagination flows. ✏️ Tip: You can customize this high-level summary in your review settings. --- autogpt_platform/frontend/package.json | 1 - autogpt_platform/frontend/pnpm-lock.yaml | 112 +----- .../components/other/EmptyTasks.tsx | 84 ++++ .../SelectedScheduleView.tsx | 2 +- .../components/SelectedScheduleActions.tsx | 40 -- .../SelectedScheduleActions.tsx | 96 +++++ .../useSelectedScheduleActions.ts | 65 +++ .../FavoritesSection/FavoritesSection.tsx | 15 +- .../LibraryActionHeader.tsx | 24 +- .../LibraryActionSubHeader.tsx | 30 +- .../LibraryAgentCard/LibraryAgentCard.tsx | 372 ++++-------------- .../components/AgentCardMenu.tsx | 188 +++++++++ .../components/FavoriteButton.tsx | 33 ++ .../components/LibraryAgentCard/helpers.ts | 150 +++++++ .../LibraryAgentCard/useLibraryAgentCard.ts | 89 +++++ .../LibraryAgentList/LibraryAgentList.tsx | 35 +- .../LibraryAgentList/useLibraryAgentList.ts | 35 +- .../LibraryNotificationCard.tsx | 175 -------- .../LibraryNotificationDropdown.tsx | 132 ------- .../LibrarySearchBar/LibrarySearchBar.tsx | 41 +- .../LibrarySearchBar/useLibrarySearchbar.tsx | 46 +-- .../LibrarySortMenu/LibrarySortMenu.tsx | 12 +- .../LibrarySortMenu/useLibrarySortMenu.ts | 10 +- .../LibraryUploadAgentDialog.tsx | 272 +++++-------- .../useLibraryUploadAgentDialog.ts | 153 +++---- .../library/components/state-provider.tsx | 59 --- .../library/components/useLibraryListPage.ts | 41 ++ .../src/app/(platform)/library/page.tsx | 23 +- .../InfiniteScroll/InfiniteScroll.tsx | 6 +- .../InfiniteScroll/useInfiniteScroll.ts | 21 +- .../useAgentActivityDropdown.ts | 18 +- .../src/components/molecules/Form/Form.tsx | 209 ++++++++++ .../src/hooks/useLibraryAgents/store.ts | 107 ----- .../useLibraryAgents/useLibraryAgents.ts | 43 +- .../frontend/src/tests/build.spec.ts | 1 + .../frontend/src/tests/library.spec.ts | 53 ++- .../frontend/src/tests/pages/library.page.ts | 75 ++-- 37 files changed, 1516 insertions(+), 1352 deletions(-) delete mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/SelectedScheduleActions.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/useSelectedScheduleActions.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/helpers.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts delete mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationCard/LibraryNotificationCard.tsx delete mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx delete mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/state-provider.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/useLibraryListPage.ts create mode 100644 autogpt_platform/frontend/src/components/molecules/Form/Form.tsx diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index fb8856a30f..f881ebaf5b 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -92,7 +92,6 @@ "react-currency-input-field": "4.0.3", "react-day-picker": "9.11.1", "react-dom": "18.3.1", - "react-drag-drop-files": "2.4.0", "react-hook-form": "7.66.0", "react-icons": "5.5.0", "react-markdown": "9.0.3", diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index 6b3e5e2ffd..4240d0d155 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -200,9 +200,6 @@ importers: react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) - react-drag-drop-files: - specifier: 2.4.0 - version: 2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-hook-form: specifier: 7.66.0 version: 7.66.0(react@18.3.1) @@ -1004,9 +1001,6 @@ packages: '@emotion/memoize@0.8.1': resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} - '@emotion/unitless@0.8.1': - resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} - '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} @@ -3122,9 +3116,6 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@types/stylis@4.2.7': - resolution: {integrity: sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==} - '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -3781,9 +3772,6 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - camelize@1.0.1: - resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001762: resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} @@ -3997,10 +3985,6 @@ packages: resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} engines: {node: '>= 0.10'} - css-color-keywords@1.0.0: - resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} - engines: {node: '>=4'} - css-loader@6.11.0: resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} engines: {node: '>= 12.13.0'} @@ -4016,9 +4000,6 @@ packages: css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - css-to-react-native@3.2.0: - resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} - css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -6131,10 +6112,6 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -6306,12 +6283,6 @@ packages: peerDependencies: react: ^18.3.1 - react-drag-drop-files@2.4.0: - resolution: {integrity: sha512-MGPV3HVVnwXEXq3gQfLtSU3jz5j5jrabvGedokpiSEMoONrDHgYl/NpIOlfsqGQ4zBv1bzzv7qbKURZNOX32PA==} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - react-hook-form@7.66.0: resolution: {integrity: sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==} engines: {node: '>=18.0.0'} @@ -6678,9 +6649,6 @@ packages: engines: {node: '>= 0.10'} hasBin: true - shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6894,13 +6862,6 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - styled-components@6.2.0: - resolution: {integrity: sha512-ryFCkETE++8jlrBmC+BoGPUN96ld1/Yp0s7t5bcXDobrs4XoXroY1tN+JbFi09hV6a5h3MzbcVi8/BGDP0eCgQ==} - engines: {node: '>= 16'} - peerDependencies: - react: '>= 16.8.0' - react-dom: '>= 16.8.0' - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -6927,9 +6888,6 @@ packages: babel-plugin-macros: optional: true - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -7096,9 +7054,6 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -8335,10 +8290,10 @@ snapshots: '@emotion/is-prop-valid@1.2.2': dependencies: '@emotion/memoize': 0.8.1 + optional: true - '@emotion/memoize@0.8.1': {} - - '@emotion/unitless@0.8.1': {} + '@emotion/memoize@0.8.1': + optional: true '@epic-web/invariant@1.0.0': {} @@ -10734,8 +10689,6 @@ snapshots: '@types/statuses@2.0.6': {} - '@types/stylis@4.2.7': {} - '@types/tedious@4.0.14': dependencies: '@types/node': 24.10.0 @@ -11432,8 +11385,6 @@ snapshots: camelcase-css@2.0.1: {} - camelize@1.0.1: {} - caniuse-lite@1.0.30001762: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -11645,8 +11596,6 @@ snapshots: randombytes: 2.1.0 randomfill: 1.0.4 - css-color-keywords@1.0.0: {} - css-loader@6.11.0(webpack@5.104.1(esbuild@0.25.12)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -11668,12 +11617,6 @@ snapshots: domutils: 2.8.0 nth-check: 2.1.1 - css-to-react-native@3.2.0: - dependencies: - camelize: 1.0.1 - css-color-keywords: 1.0.0 - postcss-value-parser: 4.2.0 - css-what@6.2.2: {} css.escape@1.5.1: {} @@ -12127,8 +12070,8 @@ snapshots: '@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -12147,7 +12090,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -12158,22 +12101,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -12184,7 +12127,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14259,12 +14202,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.49: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -14386,13 +14323,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-drag-drop-files@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-components: 6.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form@7.66.0(react@18.3.1): dependencies: react: 18.3.1 @@ -14886,8 +14816,6 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.2 - shallowequal@1.1.0: {} - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -15178,20 +15106,6 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-components@6.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@emotion/is-prop-valid': 1.2.2 - '@emotion/unitless': 0.8.1 - '@types/stylis': 4.2.7 - css-to-react-native: 3.2.0 - csstype: 3.2.3 - postcss: 8.4.49 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - shallowequal: 1.1.0 - stylis: 4.3.6 - tslib: 2.6.2 - styled-jsx@5.1.6(@babel/core@7.28.5)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -15206,8 +15120,6 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 - stylis@4.3.6: {} - sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -15390,8 +15302,6 @@ snapshots: tslib@1.14.1: {} - tslib@2.6.2: {} - tslib@2.8.1: {} tty-browserify@0.0.1: {} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTasks.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTasks.tsx index 3446611827..5ac3745bd4 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTasks.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTasks.tsx @@ -1,17 +1,25 @@ "use client"; import { getV1GetGraphVersion } from "@/app/api/__generated__/endpoints/graphs/graphs"; +import { + getGetV2ListLibraryAgentsQueryKey, + useDeleteV2DeleteLibraryAgent, +} from "@/app/api/__generated__/endpoints/library/library"; import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset"; import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; import { ShowMoreText } from "@/components/molecules/ShowMoreText/ShowMoreText"; import { useToast } from "@/components/molecules/Toast/use-toast"; import { exportAsJSONFile } from "@/lib/utils"; import { formatDate } from "@/lib/utils/time"; +import { useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; import { RunAgentModal } from "../modals/RunAgentModal/RunAgentModal"; import { RunDetailCard } from "../selected-views/RunDetailCard/RunDetailCard"; import { EmptyTasksIllustration } from "./EmptyTasksIllustration"; @@ -30,6 +38,41 @@ export function EmptyTasks({ onScheduleCreated, }: Props) { const { toast } = useToast(); + const queryClient = useQueryClient(); + const router = useRouter(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [isDeletingAgent, setIsDeletingAgent] = useState(false); + + const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent(); + + async function handleDeleteAgent() { + if (!agent.id) return; + + setIsDeletingAgent(true); + + try { + await deleteAgent({ libraryAgentId: agent.id }); + + await queryClient.refetchQueries({ + queryKey: getGetV2ListLibraryAgentsQueryKey(), + }); + + toast({ title: "Agent deleted" }); + setShowDeleteDialog(false); + router.push("/library"); + } catch (error: unknown) { + toast({ + title: "Failed to delete agent", + description: + error instanceof Error + ? error.message + : "An unexpected error occurred.", + variant: "destructive", + }); + } finally { + setIsDeletingAgent(false); + } + } async function handleExport() { try { @@ -147,9 +190,50 @@ export function EmptyTasks({ +
+ + + +
+ + Are you sure you want to delete this agent? This action cannot be + undone. + + + + + +
+
+
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx index 4cc6d8c5b0..445394c44a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx @@ -13,7 +13,7 @@ import { LoadingSelectedContent } from "../LoadingSelectedContent"; import { RunDetailCard } from "../RunDetailCard/RunDetailCard"; import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader"; import { SelectedViewLayout } from "../SelectedViewLayout"; -import { SelectedScheduleActions } from "./components/SelectedScheduleActions"; +import { SelectedScheduleActions } from "./components/SelectedScheduleActions/SelectedScheduleActions"; import { useSelectedScheduleView } from "./useSelectedScheduleView"; interface Props { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions.tsx deleted file mode 100644 index 0fd34851fd..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; -import { Button } from "@/components/atoms/Button/Button"; -import { EyeIcon } from "@phosphor-icons/react"; -import { AgentActionsDropdown } from "../../AgentActionsDropdown"; -import { useScheduleDetailHeader } from "../../RunDetailHeader/useScheduleDetailHeader"; -import { SelectedActionsWrap } from "../../SelectedActionsWrap"; - -type Props = { - agent: LibraryAgent; - scheduleId: string; - onDeleted?: () => void; -}; - -export function SelectedScheduleActions({ agent, scheduleId }: Props) { - const { openInBuilderHref } = useScheduleDetailHeader( - agent.graph_id, - scheduleId, - agent.graph_version, - ); - - return ( - <> - - {openInBuilderHref && ( - - )} - - - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/SelectedScheduleActions.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/SelectedScheduleActions.tsx new file mode 100644 index 0000000000..9088cb112b --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/SelectedScheduleActions.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { Button } from "@/components/atoms/Button/Button"; +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; +import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { EyeIcon, TrashIcon } from "@phosphor-icons/react"; +import { AgentActionsDropdown } from "../../../AgentActionsDropdown"; +import { SelectedActionsWrap } from "../../../SelectedActionsWrap"; +import { useSelectedScheduleActions } from "./useSelectedScheduleActions"; + +type Props = { + agent: LibraryAgent; + scheduleId: string; + onDeleted?: () => void; +}; + +export function SelectedScheduleActions({ + agent, + scheduleId, + onDeleted, +}: Props) { + const { + openInBuilderHref, + showDeleteDialog, + setShowDeleteDialog, + handleDelete, + isDeleting, + } = useSelectedScheduleActions({ agent, scheduleId, onDeleted }); + + return ( + <> + + {openInBuilderHref && ( + + )} + + + + + + + + Are you sure you want to delete this schedule? This action cannot be + undone. + + + + + + + + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/useSelectedScheduleActions.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/useSelectedScheduleActions.ts new file mode 100644 index 0000000000..1e22b764eb --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/components/SelectedScheduleActions/useSelectedScheduleActions.ts @@ -0,0 +1,65 @@ +"use client"; + +import { + getGetV1ListExecutionSchedulesForAGraphQueryOptions, + useDeleteV1DeleteExecutionSchedule, +} from "@/app/api/__generated__/endpoints/schedules/schedules"; +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +interface UseSelectedScheduleActionsProps { + agent: LibraryAgent; + scheduleId: string; + onDeleted?: () => void; +} + +export function useSelectedScheduleActions({ + agent, + scheduleId, + onDeleted, +}: UseSelectedScheduleActionsProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + const deleteMutation = useDeleteV1DeleteExecutionSchedule({ + mutation: { + onSuccess: () => { + toast({ title: "Schedule deleted" }); + queryClient.invalidateQueries({ + queryKey: getGetV1ListExecutionSchedulesForAGraphQueryOptions( + agent.graph_id, + ).queryKey, + }); + setShowDeleteDialog(false); + onDeleted?.(); + }, + onError: (error: unknown) => + toast({ + title: "Failed to delete schedule", + description: + error instanceof Error + ? error.message + : "An unexpected error occurred.", + variant: "destructive", + }), + }, + }); + + function handleDelete() { + if (!scheduleId) return; + deleteMutation.mutate({ scheduleId }); + } + + const openInBuilderHref = `/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`; + + return { + openInBuilderHref, + showDeleteDialog, + setShowDeleteDialog, + handleDelete, + isDeleting: deleteMutation.isPending, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx index 7ed372f296..922352292d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx @@ -1,15 +1,14 @@ "use client"; -import React from "react"; -import { useFavoriteAgents } from "../../hooks/useFavoriteAgents"; -import LibraryAgentCard from "../LibraryAgentCard/LibraryAgentCard"; -import { useGetFlag, Flag } from "@/services/feature-flags/use-get-flag"; -import { Heart } from "lucide-react"; +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { Skeleton } from "@/components/__legacy__/ui/skeleton"; import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; -import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; +import { HeartIcon } from "@phosphor-icons/react"; +import { useFavoriteAgents } from "../../hooks/useFavoriteAgents"; +import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard"; -export default function FavoritesSection() { +export function FavoritesSection() { const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING); const { allAgents: favoriteAgents, @@ -33,7 +32,7 @@ export default function FavoritesSection() { return (
- + Favorites diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionHeader/LibraryActionHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionHeader/LibraryActionHeader.tsx index 11011945fb..139f753563 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionHeader/LibraryActionHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionHeader/LibraryActionHeader.tsx @@ -1,34 +1,28 @@ -// import LibraryNotificationDropdown from "./library-notification-dropdown"; +import { LibrarySearchBar } from "../LibrarySearchBar/LibrarySearchBar"; import LibraryUploadAgentDialog from "../LibraryUploadAgentDialog/LibraryUploadAgentDialog"; -import LibrarySearchBar from "../LibrarySearchBar/LibrarySearchBar"; -type LibraryActionHeaderProps = Record; +interface Props { + setSearchTerm: (value: string) => void; +} -/** - * LibraryActionHeader component - Renders a header with search, notifications and filters - */ -const LibraryActionHeader: React.FC = ({}) => { +export function LibraryActionHeader({ setSearchTerm }: Props) { return ( <> -
- {/* */} - +
+
{/* Mobile and tablet */}
- {/* */}
- +
); -}; - -export default LibraryActionHeader; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx index be1ebe1aef..edefa52911 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx @@ -1,28 +1,28 @@ "use client"; -import LibrarySortMenu from "../LibrarySortMenu/LibrarySortMenu"; +import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; +import { Text } from "@/components/atoms/Text/Text"; +import { LibrarySortMenu } from "../LibrarySortMenu/LibrarySortMenu"; -interface LibraryActionSubHeaderProps { +interface Props { agentCount: number; + setLibrarySort: (value: LibraryAgentSort) => void; } -export default function LibraryActionSubHeader({ - agentCount, -}: LibraryActionSubHeaderProps) { +export function LibraryActionSubHeader({ agentCount, setLibrarySort }: Props) { return ( -
-
- - My agents - - +
+ My agents + - {agentCount} agents - + {agentCount} +
- +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx index ef2662f735..739eec9881 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx @@ -1,332 +1,128 @@ "use client"; -import Link from "next/link"; +import { Text } from "@/components/atoms/Text/Text"; +import { CaretCircleRightIcon } from "@phosphor-icons/react"; import Image from "next/image"; -import { Heart } from "@phosphor-icons/react"; -import { useState, useEffect } from "react"; -import { getQueryClient } from "@/lib/react-query/queryClient"; -import { InfiniteData } from "@tanstack/react-query"; +import NextLink from "next/link"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; -import { - getV2ListLibraryAgentsResponse, - getV2ListFavoriteLibraryAgentsResponse, -} from "@/app/api/__generated__/endpoints/library/library"; -import BackendAPI, { LibraryAgentID } from "@/lib/autogpt-server-api"; -import { cn } from "@/lib/utils"; -import { useToast } from "@/components/molecules/Toast/use-toast"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import Avatar, { AvatarFallback, AvatarImage, } from "@/components/atoms/Avatar/Avatar"; +import { Link } from "@/components/atoms/Link/Link"; +import { AgentCardMenu } from "./components/AgentCardMenu"; +import { FavoriteButton } from "./components/FavoriteButton"; +import { useLibraryAgentCard } from "./useLibraryAgentCard"; -interface LibraryAgentCardProps { +interface Props { agent: LibraryAgent; } -export default function LibraryAgentCard({ - agent: { - id, - name, - description, - graph_id, - can_access_graph, +export function LibraryAgentCard({ agent }: Props) { + const { id, name, graph_id, can_access_graph, image_url } = agent; + + const { + isFromMarketplace, + isAgentFavoritingEnabled, + isFavorite, + profile, creator_image_url, - image_url, - is_favorite, - }, -}: LibraryAgentCardProps) { - const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING); - const [isFavorite, setIsFavorite] = useState(is_favorite); - const [isUpdating, setIsUpdating] = useState(false); - const { toast } = useToast(); - const api = new BackendAPI(); - const queryClient = getQueryClient(); - - // Sync local state with prop when it changes (e.g., after query invalidation) - useEffect(() => { - setIsFavorite(is_favorite); - }, [is_favorite]); - - const updateQueryData = (newIsFavorite: boolean) => { - // Update the agent in all library agent queries - queryClient.setQueriesData( - { queryKey: ["/api/library/agents"] }, - ( - oldData: - | InfiniteData - | undefined, - ) => { - if (!oldData?.pages) return oldData; - - return { - ...oldData, - pages: oldData.pages.map((page) => { - if (page.status !== 200) return page; - - return { - ...page, - data: { - ...page.data, - agents: page.data.agents.map((agent: LibraryAgent) => - agent.id === id - ? { ...agent, is_favorite: newIsFavorite } - : agent, - ), - }, - }; - }), - }; - }, - ); - - // Update or remove from favorites query based on new state - queryClient.setQueriesData( - { queryKey: ["/api/library/agents/favorites"] }, - ( - oldData: - | InfiniteData< - getV2ListFavoriteLibraryAgentsResponse, - number | undefined - > - | undefined, - ) => { - if (!oldData?.pages) return oldData; - - if (newIsFavorite) { - // Add to favorites if not already there - const exists = oldData.pages.some( - (page) => - page.status === 200 && - page.data.agents.some((agent: LibraryAgent) => agent.id === id), - ); - - if (!exists) { - const firstPage = oldData.pages[0]; - if (firstPage?.status === 200) { - const updatedAgent = { - id, - name, - description, - graph_id, - can_access_graph, - creator_image_url, - image_url, - is_favorite: true, - }; - - return { - ...oldData, - pages: [ - { - ...firstPage, - data: { - ...firstPage.data, - agents: [updatedAgent, ...firstPage.data.agents], - pagination: { - ...firstPage.data.pagination, - total_items: firstPage.data.pagination.total_items + 1, - }, - }, - }, - ...oldData.pages.slice(1).map((page) => - page.status === 200 - ? { - ...page, - data: { - ...page.data, - pagination: { - ...page.data.pagination, - total_items: page.data.pagination.total_items + 1, - }, - }, - } - : page, - ), - ], - }; - } - } - } else { - // Remove from favorites - let removedCount = 0; - return { - ...oldData, - pages: oldData.pages.map((page) => { - if (page.status !== 200) return page; - - const filteredAgents = page.data.agents.filter( - (agent: LibraryAgent) => agent.id !== id, - ); - - if (filteredAgents.length < page.data.agents.length) { - removedCount = 1; - } - - return { - ...page, - data: { - ...page.data, - agents: filteredAgents, - pagination: { - ...page.data.pagination, - total_items: - page.data.pagination.total_items - removedCount, - }, - }, - }; - }), - }; - } - - return oldData; - }, - ); - }; - - const handleToggleFavorite = async (e: React.MouseEvent) => { - e.preventDefault(); // Prevent navigation when clicking the heart - e.stopPropagation(); - - if (isUpdating || !isAgentFavoritingEnabled) return; - - const newIsFavorite = !isFavorite; - - // Optimistic update - setIsFavorite(newIsFavorite); - updateQueryData(newIsFavorite); - - setIsUpdating(true); - try { - await api.updateLibraryAgent(id as LibraryAgentID, { - is_favorite: newIsFavorite, - }); - - toast({ - title: newIsFavorite ? "Added to favorites" : "Removed from favorites", - description: `${name} has been ${newIsFavorite ? "added to" : "removed from"} your favorites.`, - }); - } catch (error) { - // Revert on error - console.error("Failed to update favorite status:", error); - setIsFavorite(!newIsFavorite); - updateQueryData(!newIsFavorite); - - toast({ - title: "Error", - description: "Failed to update favorite status. Please try again.", - variant: "destructive", - }); - } finally { - setIsUpdating(false); - } - }; + handleToggleFavorite, + } = useLibraryAgentCard({ agent }); return (
- - {!image_url ? ( -
- ) : ( - {`${name} - )} - {isAgentFavoritingEnabled && ( - - )} -
- + + +
+ - {name.charAt(0)} + {name.charAt(0)} + + {isFromMarketplace ? "FROM MARKETPLACE" : "Built by you"} +
- + {isAgentFavoritingEnabled && ( + + )} +
-
- -

+
+ + {name} -

+ -

- {description} -

+ {!image_url ? ( +
+ ) : ( + {`${name} + )} -
- {/* Spacer */} - -
+
- See runs + See runs {can_access_graph && ( - Open in builder + Open in builder )}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx new file mode 100644 index 0000000000..933cdd34fa --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { + getGetV2ListLibraryAgentsQueryKey, + useDeleteV2DeleteLibraryAgent, + usePostV2ForkLibraryAgent, +} from "@/app/api/__generated__/endpoints/library/library"; +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/molecules/DropdownMenu/DropdownMenu"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { DotsThree } from "@phosphor-icons/react"; +import { useQueryClient } from "@tanstack/react-query"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +interface AgentCardMenuProps { + agent: LibraryAgent; +} + +export function AgentCardMenu({ agent }: AgentCardMenuProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const router = useRouter(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [isDeletingAgent, setIsDeletingAgent] = useState(false); + const [isDuplicatingAgent, setIsDuplicatingAgent] = useState(false); + + const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent(); + const { mutateAsync: forkAgent } = usePostV2ForkLibraryAgent(); + + async function handleDuplicateAgent() { + if (!agent.id) return; + + setIsDuplicatingAgent(true); + + try { + const result = await forkAgent({ libraryAgentId: agent.id }); + + if (result.status === 200) { + await queryClient.refetchQueries({ + queryKey: getGetV2ListLibraryAgentsQueryKey(), + }); + + toast({ + title: "Agent duplicated", + description: `${result.data.name} has been created.`, + }); + } + } catch (error: unknown) { + toast({ + title: "Failed to duplicate agent", + description: + error instanceof Error + ? error.message + : "An unexpected error occurred.", + variant: "destructive", + }); + } finally { + setIsDuplicatingAgent(false); + } + } + + async function handleDeleteAgent() { + if (!agent.id) return; + + setIsDeletingAgent(true); + + try { + await deleteAgent({ libraryAgentId: agent.id }); + + await queryClient.refetchQueries({ + queryKey: getGetV2ListLibraryAgentsQueryKey(), + }); + + toast({ title: "Agent deleted" }); + setShowDeleteDialog(false); + router.push("/library"); + } catch (error: unknown) { + toast({ + title: "Failed to delete agent", + description: + error instanceof Error + ? error.message + : "An unexpected error occurred.", + variant: "destructive", + }); + } finally { + setIsDeletingAgent(false); + } + } + + return ( + <> + + + + + + {agent.can_access_graph && ( + <> + + e.stopPropagation()} + > + Edit agent + + + + + )} + { + e.stopPropagation(); + handleDuplicateAgent(); + }} + disabled={isDuplicatingAgent} + className="flex items-center gap-2" + > + Duplicate agent + + + { + e.stopPropagation(); + setShowDeleteDialog(true); + }} + className="flex items-center gap-2 text-red-600 focus:bg-red-50 focus:text-red-600" + > + Delete agent + + + + + + +
+ + Are you sure you want to delete this agent? This action cannot be + undone. + + + + + +
+
+
+ + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx new file mode 100644 index 0000000000..518f3817c1 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { HeartIcon } from "@phosphor-icons/react"; + +interface FavoriteButtonProps { + isFavorite: boolean; + onClick: (e: React.MouseEvent) => void; +} + +export function FavoriteButton({ isFavorite, onClick }: FavoriteButtonProps) { + return ( + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/helpers.ts new file mode 100644 index 0000000000..40449e2344 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/helpers.ts @@ -0,0 +1,150 @@ +import { InfiniteData, QueryClient } from "@tanstack/react-query"; + +import { + getV2ListFavoriteLibraryAgentsResponse, + getV2ListLibraryAgentsResponse, +} from "@/app/api/__generated__/endpoints/library/library"; +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; + +interface UpdateFavoriteInQueriesParams { + queryClient: QueryClient; + agentId: string; + agent: LibraryAgent; + newIsFavorite: boolean; +} + +export function updateFavoriteInQueries({ + queryClient, + agentId, + agent, + newIsFavorite, +}: UpdateFavoriteInQueriesParams) { + queryClient.setQueriesData( + { queryKey: ["/api/library/agents"] }, + ( + oldData: + | InfiniteData + | undefined, + ) => { + if (!oldData?.pages) return oldData; + + return { + ...oldData, + pages: oldData.pages.map((page) => { + if (page.status !== 200) return page; + + return { + ...page, + data: { + ...page.data, + agents: page.data.agents.map((currentAgent: LibraryAgent) => + currentAgent.id === agentId + ? { ...currentAgent, is_favorite: newIsFavorite } + : currentAgent, + ), + }, + }; + }), + }; + }, + ); + + queryClient.setQueriesData( + { queryKey: ["/api/library/agents/favorites"] }, + ( + oldData: + | InfiniteData< + getV2ListFavoriteLibraryAgentsResponse, + number | undefined + > + | undefined, + ) => { + if (!oldData?.pages) return oldData; + + if (newIsFavorite) { + const exists = oldData.pages.some( + (page) => + page.status === 200 && + page.data.agents.some( + (currentAgent: LibraryAgent) => currentAgent.id === agentId, + ), + ); + + if (!exists) { + const firstPage = oldData.pages[0]; + if (firstPage?.status === 200) { + const updatedAgent = { + id: agent.id, + name: agent.name, + description: agent.description, + graph_id: agent.graph_id, + can_access_graph: agent.can_access_graph, + creator_image_url: agent.creator_image_url, + image_url: agent.image_url, + is_favorite: true, + }; + + return { + ...oldData, + pages: [ + { + ...firstPage, + data: { + ...firstPage.data, + agents: [updatedAgent, ...firstPage.data.agents], + pagination: { + ...firstPage.data.pagination, + total_items: firstPage.data.pagination.total_items + 1, + }, + }, + }, + ...oldData.pages.slice(1).map((page) => + page.status === 200 + ? { + ...page, + data: { + ...page.data, + pagination: { + ...page.data.pagination, + total_items: page.data.pagination.total_items + 1, + }, + }, + } + : page, + ), + ], + }; + } + } + } else { + return { + ...oldData, + pages: oldData.pages.map((page) => { + if (page.status !== 200) return page; + + const filteredAgents = page.data.agents.filter( + (currentAgent: LibraryAgent) => currentAgent.id !== agentId, + ); + + const removedCount = + filteredAgents.length < page.data.agents.length ? 1 : 0; + + return { + ...page, + data: { + ...page.data, + agents: filteredAgents, + pagination: { + ...page.data.pagination, + total_items: page.data.pagination.total_items - removedCount, + }, + }, + }; + }), + }; + } + + return oldData; + }, + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts new file mode 100644 index 0000000000..4f86b89278 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts @@ -0,0 +1,89 @@ +"use client"; + +import { getQueryClient } from "@/lib/react-query/queryClient"; +import { useEffect, useState } from "react"; + +import { usePatchV2UpdateLibraryAgent } from "@/app/api/__generated__/endpoints/library/library"; +import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store"; +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { okData } from "@/app/api/helpers"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; +import { updateFavoriteInQueries } from "./helpers"; + +interface Props { + agent: LibraryAgent; +} + +export function useLibraryAgentCard({ agent }: Props) { + const { id, name, is_favorite, creator_image_url, marketplace_listing } = + agent; + + const isFromMarketplace = Boolean(marketplace_listing); + const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING); + const [isFavorite, setIsFavorite] = useState(is_favorite); + const { toast } = useToast(); + const queryClient = getQueryClient(); + const { mutateAsync: updateLibraryAgent } = usePatchV2UpdateLibraryAgent(); + + const { data: profile } = useGetV2GetUserProfile({ + query: { + select: okData, + }, + }); + + useEffect(() => { + setIsFavorite(is_favorite); + }, [is_favorite]); + + function updateQueryData(newIsFavorite: boolean) { + updateFavoriteInQueries({ + queryClient, + agentId: id, + agent, + newIsFavorite, + }); + } + + async function handleToggleFavorite(e: React.MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + + if (!isAgentFavoritingEnabled) return; + + const newIsFavorite = !isFavorite; + + setIsFavorite(newIsFavorite); + updateQueryData(newIsFavorite); + + try { + await updateLibraryAgent({ + libraryAgentId: id, + data: { is_favorite: newIsFavorite }, + }); + + toast({ + title: newIsFavorite ? "Added to favorites" : "Removed from favorites", + description: `${name} has been ${newIsFavorite ? "added to" : "removed from"} your favorites.`, + }); + } catch { + setIsFavorite(!newIsFavorite); + updateQueryData(!newIsFavorite); + + toast({ + title: "Error", + description: "Failed to update favorite status. Please try again.", + variant: "destructive", + }); + } + } + + return { + isFromMarketplace, + isAgentFavoritingEnabled, + isFavorite, + profile, + creator_image_url, + handleToggleFavorite, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx index 1b3926a6e1..074cd8ae26 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx @@ -1,10 +1,22 @@ "use client"; -import LibraryActionSubHeader from "../LibraryActionSubHeader/LibraryActionSubHeader"; -import LibraryAgentCard from "../LibraryAgentCard/LibraryAgentCard"; +import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; +import { LibraryActionSubHeader } from "../LibraryActionSubHeader/LibraryActionSubHeader"; +import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard"; import { useLibraryAgentList } from "./useLibraryAgentList"; -export default function LibraryAgentList() { +interface Props { + searchTerm: string; + librarySort: LibraryAgentSort; + setLibrarySort: (value: LibraryAgentSort) => void; +} + +export function LibraryAgentList({ + searchTerm, + librarySort, + setLibrarySort, +}: Props) { const { agentLoading, agentCount, @@ -12,28 +24,27 @@ export default function LibraryAgentList() { hasNextPage, isFetchingNextPage, fetchNextPage, - } = useLibraryAgentList(); - - const LoadingSpinner = () => ( -
- ); + } = useLibraryAgentList({ searchTerm, librarySort }); return ( <> - +
{agentLoading ? (
- +
) : ( } + loader={} > -
+
{agents.map((agent) => ( ))} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts index e9db9a02da..0b102ff545 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts @@ -1,18 +1,23 @@ "use client"; +import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library"; +import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; import { getPaginatedTotalCount, getPaginationNextPageNumber, unpaginate, } from "@/app/api/helpers"; -import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library"; -import { useLibraryPageContext } from "../state-provider"; -import { useLibraryAgentsStore } from "@/hooks/useLibraryAgents/store"; -import { getInitialData } from "./helpers"; +import { getQueryClient } from "@/lib/react-query/queryClient"; +import { useEffect, useRef } from "react"; -export const useLibraryAgentList = () => { - const { searchTerm, librarySort } = useLibraryPageContext(); - const { agents: cachedAgents } = useLibraryAgentsStore(); +interface Props { + searchTerm: string; + librarySort: LibraryAgentSort; +} + +export function useLibraryAgentList({ searchTerm, librarySort }: Props) { + const queryClient = getQueryClient(); + const prevSortRef = useRef(null); const { data: agentsQueryData, @@ -23,18 +28,28 @@ export const useLibraryAgentList = () => { } = useGetV2ListLibraryAgentsInfinite( { page: 1, - page_size: 8, + page_size: 20, search_term: searchTerm || undefined, sort_by: librarySort, }, { query: { - initialData: getInitialData(cachedAgents, searchTerm, 8), getNextPageParam: getPaginationNextPageNumber, }, }, ); + // Reset queries when sort changes to ensure fresh data with correct sorting + useEffect(() => { + if (prevSortRef.current !== null && prevSortRef.current !== librarySort) { + // Reset all library agent queries to ensure fresh fetch with new sort + queryClient.resetQueries({ + queryKey: ["/api/library/agents"], + }); + } + prevSortRef.current = librarySort; + }, [librarySort, queryClient]); + const allAgents = agentsQueryData ? unpaginate(agentsQueryData, "agents") : []; @@ -48,4 +63,4 @@ export const useLibraryAgentList = () => { isFetchingNextPage, fetchNextPage, }; -}; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationCard/LibraryNotificationCard.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationCard/LibraryNotificationCard.tsx deleted file mode 100644 index 51520e4445..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationCard/LibraryNotificationCard.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import Image from "next/image"; -import { Button } from "@/components/__legacy__/ui/button"; -import { Separator } from "@/components/__legacy__/ui/separator"; -import { - CirclePlayIcon, - ClipboardCopy, - ImageIcon, - PlayCircle, - Share2, - X, -} from "lucide-react"; - -export interface NotificationCardData { - type: "text" | "image" | "video" | "audio"; - title: string; - id: string; - content?: string; - mediaUrl?: string; -} - -interface NotificationCardProps { - notification: NotificationCardData; - onClose: () => void; -} - -const NotificationCard = ({ - notification: { type, title, content, mediaUrl }, - onClose, -}: NotificationCardProps) => { - const barHeights = Array.from({ length: 60 }, () => - Math.floor(Math.random() * (34 - 20 + 1) + 20), - ); - - const handleClose = (e: React.MouseEvent) => { - e.preventDefault(); - onClose(); - }; - - return ( -
-
- {/* count */} -
-

- 1/4 -

-

- Success -

-
- - {/* cross icon */} - -
- -
-

- New Output Ready! -

-

- {title} -

- {type === "text" && } -
- -
- {type === "text" && ( - // Maybe in future we give markdown support -
- {content} -
- )} - - {type === "image" && - (mediaUrl ? ( -
- {title} -
- ) : ( -
- -
- ))} - - {type === "video" && ( -
- {mediaUrl ? ( -
- )} - - {type === "audio" && ( -
- -
- {/*
- )} -
- -
-
- - -
- -
-
- ); -}; - -export default NotificationCard; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx deleted file mode 100644 index cd863a21af..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client"; -import React, { useState, useEffect, useMemo } from "react"; - -import { motion, useAnimationControls } from "framer-motion"; -import { BellIcon, X } from "lucide-react"; -import { Button } from "@/components/__legacy__/Button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/__legacy__/ui/dropdown-menu"; -import NotificationCard, { - NotificationCardData, -} from "../LibraryNotificationCard/LibraryNotificationCard"; - -export default function LibraryNotificationDropdown(): React.ReactNode { - const controls = useAnimationControls(); - const [open, setOpen] = useState(false); - const [notifications, setNotifications] = useState< - NotificationCardData[] | null - >(null); - - const initialNotificationData = useMemo( - () => - [ - { - type: "audio", - title: "Audio Processing Complete", - id: "4", - }, - { - type: "text", - title: "LinkedIn Post Generator: YouTube to Professional Content", - id: "1", - content: - "As artificial intelligence (AI) continues to evolve, it's increasingly clear that AI isn't just a trend—it's reshaping the way we work, innovate, and solve complex problems. However, for many professionals, the question remains: How can I leverage AI to drive meaningful results in my own field? In this article, we'll explore how AI can empower businesses and individuals alike to be more efficient, make better decisions, and unlock new opportunities. Whether you're in tech, finance, healthcare, or any other industry, understanding the potential of AI can set you apart.", - }, - { - type: "image", - title: "New Image Upload", - id: "2", - }, - { - type: "video", - title: "Video Processing Complete", - id: "3", - }, - ] as NotificationCardData[], - [], - ); - - useEffect(() => { - if (initialNotificationData) { - setNotifications(initialNotificationData); - } - }, [initialNotificationData]); - - const handleHoverStart = () => { - controls.start({ - rotate: [0, -10, 10, -10, 10, 0], - transition: { duration: 0.5 }, - }); - }; - - return ( - - - - - - - Agent run updates - - -
- {notifications && notifications.length ? ( - notifications.map((notification) => ( - - - setNotifications((prev) => { - if (!prev) return null; - return prev.filter((n) => n.id !== notification.id); - }) - } - /> - - )) - ) : ( -
- No notifications present -
- )} -
-
-
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx index ee36347874..cf63c300d6 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx @@ -1,40 +1,37 @@ "use client"; -import { Input } from "@/components/__legacy__/ui/input"; -import { Search, X } from "lucide-react"; + +import { Input } from "@/components/atoms/Input/Input"; +import { MagnifyingGlassIcon } from "@phosphor-icons/react"; import { useLibrarySearchbar } from "./useLibrarySearchbar"; -export default function LibrarySearchBar(): React.ReactNode { - const { handleSearchInput, handleClear, setIsFocused, isFocused, inputRef } = - useLibrarySearchbar(); +interface Props { + setSearchTerm: (value: string) => void; +} + +export function LibrarySearchBar({ setSearchTerm }: Props) { + const { handleSearchInput } = useLibrarySearchbar({ setSearchTerm }); + return (
inputRef.current?.focus()} - className="relative z-[21] mx-auto flex h-[50px] w-full max-w-[500px] flex-1 cursor-pointer items-center rounded-[45px] bg-[#EDEDED] px-[24px] py-[10px]" + className="relative z-[21] -mb-6 flex w-full items-center md:w-auto" > - setIsFocused(true)} - onBlur={() => !inputRef.current?.value && setIsFocused(false)} + label="Search agents" + id="library-search-bar" + hideLabel onChange={handleSearchInput} - className="flex-1 border-none font-sans text-[16px] font-normal leading-7 shadow-none focus:shadow-none focus:ring-0" + className="min-w-[18rem] pl-12 lg:min-w-[30rem]" type="text" data-testid="library-textbox" placeholder="Search agents" /> - - {isFocused && inputRef.current?.value && ( - - )}
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx index f6428c6c4e..74b8e9874c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx @@ -1,36 +1,30 @@ -import { useRef, useState } from "react"; -import { useLibraryPageContext } from "../state-provider"; import { debounce } from "lodash"; +import { useCallback, useEffect } from "react"; -export const useLibrarySearchbar = () => { - const inputRef = useRef(null); - const [isFocused, setIsFocused] = useState(false); - const { setSearchTerm } = useLibraryPageContext(); +interface Props { + setSearchTerm: (value: string) => void; +} - const debouncedSearch = debounce((value: string) => { - setSearchTerm(value); - }, 300); +export function useLibrarySearchbar({ setSearchTerm }: Props) { + const debouncedSearch = useCallback( + debounce((value: string) => { + setSearchTerm(value); + }, 300), + [setSearchTerm], + ); - const handleSearchInput = (e: React.ChangeEvent) => { + useEffect(() => { + return () => { + debouncedSearch.cancel(); + }; + }, [debouncedSearch]); + + function handleSearchInput(e: React.ChangeEvent) { const searchTerm = e.target.value; debouncedSearch(searchTerm); - }; - - const handleClear = (e: React.MouseEvent) => { - if (inputRef.current) { - inputRef.current.value = ""; - inputRef.current.blur(); - setSearchTerm(""); - e.preventDefault(); - } - setIsFocused(false); - }; + } return { - handleClear, handleSearchInput, - isFocused, - inputRef, - setIsFocused, }; -}; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx index ac4ed060f2..de37af5fad 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx @@ -1,5 +1,5 @@ "use client"; -import { ArrowDownNarrowWideIcon } from "lucide-react"; +import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; import { Select, SelectContent, @@ -8,11 +8,15 @@ import { SelectTrigger, SelectValue, } from "@/components/__legacy__/ui/select"; -import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; +import { ArrowDownNarrowWideIcon } from "lucide-react"; import { useLibrarySortMenu } from "./useLibrarySortMenu"; -export default function LibrarySortMenu(): React.ReactNode { - const { handleSortChange } = useLibrarySortMenu(); +interface Props { + setLibrarySort: (value: LibraryAgentSort) => void; +} + +export function LibrarySortMenu({ setLibrarySort }: Props) { + const { handleSortChange } = useLibrarySortMenu({ setLibrarySort }); return (
sort by diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts index d2575c8936..e6d6f2d127 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts @@ -1,11 +1,11 @@ import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; -import { useLibraryPageContext } from "../state-provider"; -export const useLibrarySortMenu = () => { - const { setLibrarySort } = useLibraryPageContext(); +interface Props { + setLibrarySort: (value: LibraryAgentSort) => void; +} +export function useLibrarySortMenu({ setLibrarySort }: Props) { const handleSortChange = (value: LibraryAgentSort) => { - // Simply updating the sort state - React Query will handle the rest setLibrarySort(value); }; @@ -24,4 +24,4 @@ export const useLibrarySortMenu = () => { handleSortChange, getSortLabel, }; -}; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx index d92bbe86fe..1a6999721e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx @@ -1,192 +1,134 @@ "use client"; -import { Upload, X } from "lucide-react"; -import { Button } from "@/components/__legacy__/Button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/__legacy__/ui/dialog"; -import { z } from "zod"; -import { FileUploader } from "react-drag-drop-files"; +import { Button } from "@/components/atoms/Button/Button"; +import { FileInput } from "@/components/atoms/FileInput/FileInput"; +import { Input } from "@/components/atoms/Input/Input"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; import { Form, FormControl, FormField, FormItem, - FormLabel, FormMessage, -} from "@/components/__legacy__/ui/form"; -import { Input } from "@/components/__legacy__/ui/input"; -import { Textarea } from "@/components/__legacy__/ui/textarea"; +} from "@/components/molecules/Form/Form"; +import { UploadSimpleIcon } from "@phosphor-icons/react"; +import { z } from "zod"; import { useLibraryUploadAgentDialog } from "./useLibraryUploadAgentDialog"; -const fileTypes = ["JSON"]; - -const fileSchema = z.custom((val) => val instanceof File, { - message: "Must be a File object", -}); - export const uploadAgentFormSchema = z.object({ - agentFile: fileSchema, + agentFile: z.string().min(1, "Agent file is required"), agentName: z.string().min(1, "Agent name is required"), agentDescription: z.string(), }); -export default function LibraryUploadAgentDialog(): React.ReactNode { - const { - onSubmit, - isUploading, - isOpen, - setIsOpen, - isDroped, - handleChange, - form, - setisDroped, - agentObject, - clearAgentFile, - } = useLibraryUploadAgentDialog(); +export default function LibraryUploadAgentDialog() { + const { onSubmit, isUploading, isOpen, setIsOpen, form, agentObject } = + useLibraryUploadAgentDialog(); + return ( - - + { + setIsOpen(false); + }} + > + - - - - Upload Agent - - Upload your agent by providing a name, description, and JSON file. - - + + + + ( + + + + + + + )} + /> - - - ( - - Agent name - - - - - - )} - /> + ( + + + + + + + )} + /> - ( - - Description - -