feat(builder): Block menu redesign - part 3 (#10864)
### Changes 🏗️ #### Block Menu Redesign - Part 3 This PR continues the block menu redesign effort, implementing the new content sections and improving the overall user experience. The changes focus on better organization, pagination, error handling, and visual consistency. #### Key Features Implemented: **1. New Content Organization** - **All Blocks Content**: Complete listing of all available blocks with category-based organization and infinite scroll support (`AllBlocksContent/`) - **My Agents Content**: Display and manage user's own agents with pagination (`MyAgentsContent/`) - **Marketplace Agents Content**: Browse and add marketplace agents with improved loading states (`MarketplaceAgentsContent/`) - **Integration Blocks**: Dedicated view for integration-specific blocks with better filtering (`IntegrationBlocks/`) - **Suggestion Content**: Smart suggestions based on user context and search history (`SuggestionContent/`) - **Integrations Content**: Browse available integrations in a dedicated view (`IntegrationsContent/`) **2. Enhanced UI Components** - **Paginated Lists**: New pagination components for blocks and integrations (`PaginatedBlocksContent/`, `PaginatedIntegrationList/`) - **Block List**: Reusable block list component with consistent styling (`BlockList/`) - **Improved Error Handling**: Comprehensive error states with retry functionality across all content types - **Loading States**: Skeleton loaders for better perceived performance **3. Infrastructure Improvements** - **Centralized Styles**: New `style.ts` file for consistent styling across components - **Better State Management**: Enhanced context provider with improved menu state handling - **Mock Flag Support**: Added feature flags for testing new block features - **Default State Enum**: Refactored to use enums for menu default states **4. Visual Assets** - Added 50+ new integration icons/logos for better visual representation - Updated existing integration images for consistency **5. Code Quality** - Improved error handling with proper error cards and retry mechanisms - Consistent formatting and import organization - Enhanced TypeScript types and interfaces - Better separation of concerns with dedicated hooks for each content type #### Technical Details: - **Files Changed**: 96 files - **Additions**: 1,380 lines - **Deletions**: 162 lines - **New Components**: 10+ new React components with dedicated hooks - **Integration Icons**: 50+ new PNG images for various integrations #### Breaking Changes: None - All changes are backwards compatible --- ### Test Plan 📋 - [x] Create a new agent and verify all blocks are accessible - [x] Test infinite scroll in "All Blocks" view - [x] Verify pagination works correctly in marketplace agents view - [x] Test error states by simulating network failures - [x] Check that all new integration icons display correctly - [x] Test adding agents from marketplace view - [x] Ensure skeleton loaders appear during data fetching > Generated by claude
@@ -41,6 +41,26 @@ export default defineConfig({
|
||||
useInfiniteQueryParam: "page",
|
||||
},
|
||||
},
|
||||
"getV2Get builder blocks": {
|
||||
query: {
|
||||
useInfinite: true,
|
||||
useInfiniteQueryParam: "page",
|
||||
useQuery: true,
|
||||
},
|
||||
},
|
||||
"getV2Get builder integration providers": {
|
||||
query: {
|
||||
useInfinite: true,
|
||||
useInfiniteQueryParam: "page",
|
||||
},
|
||||
},
|
||||
"getV2List store agents": {
|
||||
query: {
|
||||
useInfinite: true,
|
||||
useInfiniteQueryParam: "page",
|
||||
useQuery: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
BIN
autogpt_platform/frontend/public/integrations/aiml_api.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
autogpt_platform/frontend/public/integrations/airtable.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
autogpt_platform/frontend/public/integrations/anthropic.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
autogpt_platform/frontend/public/integrations/apollo.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
autogpt_platform/frontend/public/integrations/baas.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
autogpt_platform/frontend/public/integrations/bannerbear.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/d_id.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
autogpt_platform/frontend/public/integrations/dataforseo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
autogpt_platform/frontend/public/integrations/e2b.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/enrichlayer.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
autogpt_platform/frontend/public/integrations/exa.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
autogpt_platform/frontend/public/integrations/fal.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
autogpt_platform/frontend/public/integrations/firecrawl.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 13 KiB |
BIN
autogpt_platform/frontend/public/integrations/google_maps.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
autogpt_platform/frontend/public/integrations/groq.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
autogpt_platform/frontend/public/integrations/http.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
autogpt_platform/frontend/public/integrations/ideogram.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
autogpt_platform/frontend/public/integrations/jina.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
autogpt_platform/frontend/public/integrations/llama_api.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 21 KiB |
BIN
autogpt_platform/frontend/public/integrations/nvidia.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
autogpt_platform/frontend/public/integrations/ollama.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
autogpt_platform/frontend/public/integrations/open_router.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/openai.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
autogpt_platform/frontend/public/integrations/replicate.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
autogpt_platform/frontend/public/integrations/revid.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
autogpt_platform/frontend/public/integrations/screenshotone.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
autogpt_platform/frontend/public/integrations/slant3d.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
autogpt_platform/frontend/public/integrations/smartlead.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 3.7 KiB |
BIN
autogpt_platform/frontend/public/integrations/stagehand.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 40 KiB |
BIN
autogpt_platform/frontend/public/integrations/twitter.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
autogpt_platform/frontend/public/integrations/unreal_speech.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
autogpt_platform/frontend/public/integrations/v0.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
autogpt_platform/frontend/public/integrations/wolfram.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
autogpt_platform/frontend/public/integrations/zerobounce.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
@@ -60,4 +60,4 @@ export const AiBlock: React.FC<Props> = ({
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Block } from "../Block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { useAllBlockContent } from "./useAllBlockContent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
export const AllBlocksContent = () => {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
handleRefetchBlocks,
|
||||
isLoadingMore,
|
||||
isErrorOnLoadingMore,
|
||||
} = useAllBlockContent();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
{[0, 1, 2, 3, 4].map((skeletonIndex) => (
|
||||
<Block.Skeleton key={`skeleton-${skeletonIndex}`} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="h-full p-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={error || undefined}
|
||||
httpError={{
|
||||
status: data?.status,
|
||||
statusText: "Request failed",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
context="block menu"
|
||||
onRetry={() => window.location.reload()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = data?.categories;
|
||||
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
{categories?.map((category, index) => (
|
||||
<Fragment key={category.name}>
|
||||
{index > 0 && <Separator className="h-[1px] w-full text-zinc-300" />}
|
||||
|
||||
{/* Category Section */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
{category.name && beautifyString(category.name)}
|
||||
</p>
|
||||
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
|
||||
{category.total_blocks}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{category.blocks.map((block) => (
|
||||
<Block
|
||||
key={`${category.name}-${block.id}`}
|
||||
title={block.name as string}
|
||||
description={block.name as string}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isLoadingMore(category.name) && (
|
||||
<>
|
||||
{[0, 1, 2].map((skeletonIndex) => (
|
||||
<Block.Skeleton
|
||||
key={`skeleton-${category.name}-${skeletonIndex}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isErrorOnLoadingMore && (
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={{ message: "Error loading blocks" }}
|
||||
context="blocks"
|
||||
onRetry={() => handleRefetchBlocks(category.name)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{category.total_blocks > category.blocks.length && (
|
||||
<Button
|
||||
variant={"link"}
|
||||
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
|
||||
disabled={isLoadingMore(category.name)}
|
||||
onClick={() => handleRefetchBlocks(category.name)}
|
||||
>
|
||||
see all
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
getGetV2GetBuilderBlockCategoriesQueryKey,
|
||||
getV2GetBuilderBlocks,
|
||||
useGetV2GetBuilderBlockCategories,
|
||||
} from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { BlockCategoryResponse } from "@/app/api/__generated__/models/blockCategoryResponse";
|
||||
import { BlockResponse } from "@/app/api/__generated__/models/blockResponse";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { getQueryClient } from "@/lib/react-query/queryClient";
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
|
||||
export const useAllBlockContent = () => {
|
||||
const { toast } = useToast();
|
||||
const [loadingCategories, setLoadingCategories] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [errorLoadingCategories, setErrorLoadingCategories] = useState<
|
||||
Set<string>
|
||||
>(new Set());
|
||||
|
||||
const { data, isLoading, isError, error } = useGetV2GetBuilderBlockCategories(
|
||||
undefined,
|
||||
{
|
||||
query: {
|
||||
select: (x) => {
|
||||
return {
|
||||
categories: x.data as BlockCategoryResponse[],
|
||||
status: x.status,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleRefetchBlocks = async (targetCategory: string) => {
|
||||
try {
|
||||
setLoadingCategories((prev) => new Set(prev).add(targetCategory));
|
||||
|
||||
// Clear any previous error for this category
|
||||
setErrorLoadingCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(targetCategory);
|
||||
return newSet;
|
||||
});
|
||||
const response = await getV2GetBuilderBlocks({
|
||||
category: targetCategory,
|
||||
});
|
||||
|
||||
const result = response.data as BlockResponse;
|
||||
if (result.blocks) {
|
||||
const categoriesQueryKey = getGetV2GetBuilderBlockCategoriesQueryKey();
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(categoriesQueryKey, (old: any) => {
|
||||
if (!old?.data) return old;
|
||||
const categories = old.data as BlockCategoryResponse[];
|
||||
|
||||
const updatedCategories = categories.map((old_cat) => {
|
||||
if (old_cat.name === targetCategory) {
|
||||
return {
|
||||
...old_cat,
|
||||
blocks: result.blocks,
|
||||
};
|
||||
}
|
||||
return old_cat;
|
||||
});
|
||||
return {
|
||||
...old,
|
||||
data: updatedCategories,
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
setErrorLoadingCategories((prev) => new Set(prev).add(targetCategory));
|
||||
toast({
|
||||
title: "Error loading blocks",
|
||||
description: "Please try again later",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoadingCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(targetCategory);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isLoadingMore = (categoryName: string) =>
|
||||
loadingCategories.has(categoryName);
|
||||
const isErrorOnLoadingMore = (categoryName: string) =>
|
||||
errorLoadingCategories.has(categoryName);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
handleRefetchBlocks,
|
||||
isLoadingMore,
|
||||
isErrorOnLoadingMore,
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import React, { ButtonHTMLAttributes } from "react";import { highlightText } from "./helpers";
|
||||
;
|
||||
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { highlightText } from "./helpers";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@@ -56,7 +55,7 @@ export const Block: BlockComponent = ({
|
||||
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
|
||||
)}
|
||||
>
|
||||
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
|
||||
<PlusIcon className="h-5 w-5 text-zinc-50" />
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
@@ -74,4 +73,4 @@ const BlockSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
Block.Skeleton = BlockSkeleton;
|
||||
Block.Skeleton = BlockSkeleton;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { Block } from "../Block";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
export interface BlockType {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
type?: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
interface BlocksListProps {
|
||||
blocks: BlockType[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const BlocksList: React.FC<BlocksListProps> = ({
|
||||
blocks,
|
||||
loading = false,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
{Array.from({ length: 7 }).map((_, index) => (
|
||||
<Block.Skeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return blocks.map((block) => (
|
||||
<Block key={block.id} title={block.name} description={block.description} />
|
||||
));
|
||||
};
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ToyBrick } from "lucide-react";
|
||||
import { BlockMenuContent } from "../BlockMenuContent/BlockMenuContent";
|
||||
import { ControlPanelButton } from "../ControlPanelButton";
|
||||
import { useBlockMenu } from "./useBlockMenu";
|
||||
import { BlockMenuStateProvider } from "../block-menu-provider";
|
||||
import { LegoIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface BlockMenuProps {
|
||||
pinBlocksPopover: boolean;
|
||||
@@ -23,7 +23,10 @@ export const BlockMenu: React.FC<BlockMenuProps> = ({
|
||||
blockMenuSelected,
|
||||
setBlockMenuSelected,
|
||||
}) => {
|
||||
const {open, onOpen} = useBlockMenu({pinBlocksPopover, setBlockMenuSelected});
|
||||
const { open, onOpen } = useBlockMenu({
|
||||
pinBlocksPopover,
|
||||
setBlockMenuSelected,
|
||||
});
|
||||
return (
|
||||
<Popover open={pinBlocksPopover ? true : open} onOpenChange={onOpen}>
|
||||
<PopoverTrigger className="hover:cursor-pointer">
|
||||
@@ -33,8 +36,8 @@ export const BlockMenu: React.FC<BlockMenuProps> = ({
|
||||
selected={blockMenuSelected === "block"}
|
||||
className="rounded-none"
|
||||
>
|
||||
{/* Need to find phosphor icon alternative for this lucide icon */}
|
||||
<ToyBrick className="h-5 w-6" strokeWidth={2} />
|
||||
{/* Need to find phosphor icon alternative for this lucide icon */}
|
||||
<LegoIcon className="h-6 w-6" />
|
||||
</ControlPanelButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -42,14 +45,13 @@ export const BlockMenu: React.FC<BlockMenuProps> = ({
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={16}
|
||||
className="absolute h-[75vh] w-[46.625rem] overflow-hidden rounded-[1rem] border-none p-0 shadow-[0_2px_6px_0_rgba(0,0,0,0.05)]"
|
||||
className="absolute h-[80vh] w-[46.625rem] overflow-hidden rounded-[1rem] border-none p-0 shadow-[0_2px_6px_0_rgba(0,0,0,0.05)]"
|
||||
data-id="blocks-control-popover-content"
|
||||
>
|
||||
<BlockMenuStateProvider>
|
||||
<BlockMenuContent />
|
||||
</BlockMenuStateProvider>
|
||||
|
||||
<BlockMenuStateProvider>
|
||||
<BlockMenuContent />
|
||||
</BlockMenuStateProvider>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface useBlockMenuProps {
|
||||
pinBlocksPopover: boolean;
|
||||
setBlockMenuSelected: React.Dispatch<
|
||||
pinBlocksPopover: boolean;
|
||||
setBlockMenuSelected: React.Dispatch<
|
||||
React.SetStateAction<"" | "save" | "block" | "search">
|
||||
>;
|
||||
}
|
||||
|
||||
export const useBlockMenu = ({pinBlocksPopover, setBlockMenuSelected}: useBlockMenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onOpen = (newOpen: boolean) => {
|
||||
if (!pinBlocksPopover) {
|
||||
setOpen(newOpen);
|
||||
setBlockMenuSelected(newOpen ? "block" : "");
|
||||
}
|
||||
};
|
||||
export const useBlockMenu = ({
|
||||
pinBlocksPopover,
|
||||
setBlockMenuSelected,
|
||||
}: useBlockMenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onOpen = (newOpen: boolean) => {
|
||||
if (!pinBlocksPopover) {
|
||||
setOpen(newOpen);
|
||||
setBlockMenuSelected(newOpen ? "block" : "");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
onOpen,
|
||||
};
|
||||
};
|
||||
return {
|
||||
open,
|
||||
onOpen,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,4 +12,4 @@ export const BlockMenuDefault = () => {
|
||||
<BlockMenuDefaultContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import React from "react";
|
||||
import { DefaultStateType, useBlockMenuContext } from "../block-menu-provider";
|
||||
import { AllBlocksContent } from "../AllBlocksContent/AllBlocksContent";
|
||||
import { PaginatedBlocksContent } from "../PaginatedBlocksContent/PaginatedBlocksContent";
|
||||
import { IntegrationsContent } from "../IntegrationsContent/IntegrationsContent";
|
||||
import { MarketplaceAgentsContent } from "../MarketplaceAgentsContent/MarketplaceAgentsContent";
|
||||
import { MyAgentsContent } from "../MyAgentsContent/MyAgentsContent";
|
||||
import { SuggestionContent } from "../SuggestionContent/SuggestionContent";
|
||||
|
||||
export const BlockMenuDefaultContent = () => {
|
||||
const { defaultState } = useBlockMenuContext();
|
||||
|
||||
return (
|
||||
<div className="h-full flex-1 overflow-hidden flex items-center justify-center">
|
||||
{/* I have added temporary content here, will fillup it in follow up prs */}
|
||||
<Text variant="body" className="text-green-300">
|
||||
This is the block menu default content
|
||||
</Text>
|
||||
<div className="h-full flex-1 overflow-hidden">
|
||||
{defaultState == DefaultStateType.SUGGESTION && <SuggestionContent />}
|
||||
{defaultState == DefaultStateType.ALL_BLOCKS && <AllBlocksContent />}
|
||||
{defaultState == DefaultStateType.INPUT_BLOCKS && (
|
||||
<PaginatedBlocksContent type="input" />
|
||||
)}
|
||||
{defaultState == DefaultStateType.ACTION_BLOCKS && (
|
||||
<PaginatedBlocksContent type="action" />
|
||||
)}
|
||||
{defaultState == DefaultStateType.OUTPUT_BLOCKS && (
|
||||
<PaginatedBlocksContent type="output" />
|
||||
)}
|
||||
{defaultState == DefaultStateType.INTEGRATIONS && <IntegrationsContent />}
|
||||
{defaultState == DefaultStateType.MARKETPLACE_AGENTS && (
|
||||
<MarketplaceAgentsContent />
|
||||
)}
|
||||
{defaultState == DefaultStateType.MY_AGENTS && <MyAgentsContent />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Text } from "@/components/atoms/Text/Text";
|
||||
export const BlockMenuSearch = () => {
|
||||
return (
|
||||
// This is just a temporary text, will content inside in it [in follow-up prs]
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Text variant="h3" className="text-green-300">
|
||||
This is the block menu search
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,7 +12,13 @@ interface BlockMenuSearchBarProps {
|
||||
export const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
|
||||
className = "",
|
||||
}) => {
|
||||
const { handleClear, inputRef, localQuery, setLocalQuery, debouncedSetSearchQuery } = useBlockMenuSearchBar();
|
||||
const {
|
||||
handleClear,
|
||||
inputRef,
|
||||
localQuery,
|
||||
setLocalQuery,
|
||||
debouncedSetSearchQuery,
|
||||
} = useBlockMenuSearchBar();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -22,7 +28,10 @@ export const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
<MagnifyingGlassIcon className="h-6 w-6 text-zinc-700" strokeWidth={2} />
|
||||
<MagnifyingGlassIcon
|
||||
className="h-6 w-6 text-zinc-700"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
@@ -50,4 +59,4 @@ export const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
import { debounce } from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
export const useBlockMenuSearchBar = () => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [localQuery, setLocalQuery] = useState("");
|
||||
const { setSearchQuery, setSearchId, searchId } = useBlockMenuContext();
|
||||
|
||||
const searchIdRef = useRef(searchId);
|
||||
useEffect(() => {
|
||||
searchIdRef.current = searchId;
|
||||
}, [searchId]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [localQuery, setLocalQuery] = useState("");
|
||||
const { setSearchQuery, setSearchId, searchId } = useBlockMenuContext();
|
||||
|
||||
const debouncedSetSearchQuery = debounce((value: string) => {
|
||||
setSearchQuery(value);
|
||||
if (value.length === 0) {
|
||||
setSearchId(undefined);
|
||||
} else if (!searchIdRef.current) {
|
||||
setSearchId(crypto.randomUUID());
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
}, [debouncedSetSearchQuery]);
|
||||
|
||||
const handleClear = () => {
|
||||
setLocalQuery("");
|
||||
setSearchQuery("");
|
||||
setSearchId(undefined);
|
||||
const searchIdRef = useRef(searchId);
|
||||
useEffect(() => {
|
||||
searchIdRef.current = searchId;
|
||||
}, [searchId]);
|
||||
|
||||
const debouncedSetSearchQuery = useCallback(
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value);
|
||||
if (value.length === 0) {
|
||||
setSearchId(undefined);
|
||||
} else if (!searchIdRef.current) {
|
||||
setSearchId(crypto.randomUUID());
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS),
|
||||
[setSearchQuery, setSearchId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
}, [debouncedSetSearchQuery]);
|
||||
|
||||
return {
|
||||
handleClear,
|
||||
inputRef,
|
||||
localQuery,
|
||||
setLocalQuery,
|
||||
debouncedSetSearchQuery,
|
||||
}
|
||||
};
|
||||
const handleClear = () => {
|
||||
setLocalQuery("");
|
||||
setSearchQuery("");
|
||||
setSearchId(undefined);
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
|
||||
return {
|
||||
handleClear,
|
||||
inputRef,
|
||||
localQuery,
|
||||
setLocalQuery,
|
||||
debouncedSetSearchQuery,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from "react";
|
||||
import { MenuItem } from "../MenuItem";
|
||||
import { DefaultStateType } from "../block-menu-provider";
|
||||
import { DefaultStateType, useBlockMenuContext } from "../block-menu-provider";
|
||||
import { useBlockMenuSidebar } from "./useBlockMenuSidebar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
|
||||
export const BlockMenuSidebar = () => {
|
||||
const { blockCounts, setDefaultState, defaultState, isLoading, isError, error } = useBlockMenuSidebar();
|
||||
|
||||
const { data, setDefaultState, defaultState, isLoading, isError, error } =
|
||||
useBlockMenuSidebar();
|
||||
const { setIntegration } = useBlockMenuContext();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-fit space-y-2 px-4 pt-4">
|
||||
@@ -21,11 +22,25 @@ export const BlockMenuSidebar = () => {
|
||||
);
|
||||
}
|
||||
if (isError) {
|
||||
return <div className="w-fit space-y-2 px-4 pt-4">
|
||||
<ErrorCard className="w-[12.875rem]" httpError={{status: 500, statusText: "Internal Server Error", message: error?.detail || 'An error occurred'}} />
|
||||
return (
|
||||
<div className="w-fit space-y-2 px-4 pt-4">
|
||||
<ErrorCard
|
||||
className="w-[12.875rem]"
|
||||
isSuccess={false}
|
||||
responseError={error || undefined}
|
||||
context="block menu"
|
||||
httpError={{
|
||||
status: data?.status,
|
||||
statusText: "Internal Server Error",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const blockCounts = data?.blockCounts;
|
||||
|
||||
const topLevelMenuItems = [
|
||||
{
|
||||
name: "Suggestion",
|
||||
@@ -62,7 +77,8 @@ export const BlockMenuSidebar = () => {
|
||||
type: "integrations",
|
||||
number: blockCounts?.integrations,
|
||||
onClick: () => {
|
||||
setDefaultState("integrations");
|
||||
setDefaultState(DefaultStateType.INTEGRATIONS);
|
||||
setIntegration(undefined);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -114,4 +130,4 @@ export const BlockMenuSidebar = () => {
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,20 +5,23 @@ import { CountResponse } from "@/app/api/__generated__/models/countResponse";
|
||||
export const useBlockMenuSidebar = () => {
|
||||
const { defaultState, setDefaultState } = useBlockMenuContext();
|
||||
|
||||
const { data: blockCounts, isLoading, isError, error} = useGetV2GetBuilderItemCounts({
|
||||
query : {
|
||||
select : (x) =>{
|
||||
return x.data as CountResponse
|
||||
}
|
||||
}
|
||||
const { data, isLoading, isError, error } = useGetV2GetBuilderItemCounts({
|
||||
query: {
|
||||
select: (x) => {
|
||||
return {
|
||||
blockCounts: x.data as CountResponse,
|
||||
status: x.status,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
blockCounts,
|
||||
data,
|
||||
setDefaultState,
|
||||
defaultState,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,4 +32,4 @@ export const ControlPanelButton: React.FC<Props> = ({
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -51,4 +51,4 @@ export const FilterChip: React.FC<Props> = ({
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -85,4 +85,4 @@ const IntegrationSkeleton: React.FC<{ className?: string }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
Integration.Skeleton = IntegrationSkeleton;
|
||||
Integration.Skeleton = IntegrationSkeleton;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React, { Fragment } from "react";
|
||||
import { IntegrationBlock } from "../IntergrationBlock";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useIntegrationBlocks } from "./useIntegrationBlocks";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
|
||||
export const IntegrationBlocks = () => {
|
||||
const { integration, setIntegration } = useBlockMenuContext();
|
||||
const {
|
||||
allBlocks,
|
||||
status,
|
||||
totalBlocks,
|
||||
blocksLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
refetch,
|
||||
} = useIntegrationBlocks();
|
||||
|
||||
if (blocksLoading) {
|
||||
return (
|
||||
<div className="w-full space-y-3 p-4">
|
||||
{Array.from({ length: 3 }).map((_, blockIndex) => (
|
||||
<Fragment key={blockIndex}>
|
||||
{blockIndex > 0 && (
|
||||
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
|
||||
)}
|
||||
{[0, 1, 2].map((index) => (
|
||||
<IntegrationBlock.Skeleton key={`${blockIndex}-${index}`} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full p-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={error || undefined}
|
||||
httpError={{
|
||||
status: status,
|
||||
statusText: "Request failed",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
context="block menu"
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
>
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={"link"}
|
||||
className="p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800"
|
||||
onClick={() => {
|
||||
setIntegration(undefined);
|
||||
}}
|
||||
>
|
||||
Integrations
|
||||
</Button>
|
||||
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
/
|
||||
</p>
|
||||
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
{integration}
|
||||
</p>
|
||||
</div>
|
||||
<span className="flex h-[1.375rem] w-[1.6875rem] items-center justify-center rounded-[1.25rem] bg-[#f0f0f0] p-1.5 font-sans text-sm leading-[1.375rem] text-zinc-500 group-disabled:text-zinc-400">
|
||||
{totalBlocks}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{allBlocks.map((block) => (
|
||||
<IntegrationBlock
|
||||
key={block.id}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
icon_url={`/integrations/${integration}.png`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useGetV2GetBuilderBlocksInfinite } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { BlockResponse } from "@/app/api/__generated__/models/blockResponse";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export const useIntegrationBlocks = () => {
|
||||
const { integration } = useBlockMenuContext();
|
||||
|
||||
const {
|
||||
data: blocks,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isLoading: blocksLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetV2GetBuilderBlocksInfinite(
|
||||
{
|
||||
page: 1,
|
||||
page_size: PAGE_SIZE,
|
||||
provider: integration,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as BlockResponse).pagination;
|
||||
const isMore =
|
||||
pagination.current_page * pagination.page_size <
|
||||
pagination.total_items;
|
||||
|
||||
return isMore ? pagination.current_page + 1 : undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const allBlocks =
|
||||
blocks?.pages?.flatMap((page) => {
|
||||
const response = page.data as BlockResponse;
|
||||
return response.blocks;
|
||||
}) ?? [];
|
||||
|
||||
const totalBlocks = blocks?.pages[0]
|
||||
? (blocks.pages[0].data as BlockResponse).pagination.total_items
|
||||
: 0;
|
||||
|
||||
const status = blocks?.pages[0]?.status;
|
||||
|
||||
return {
|
||||
allBlocks,
|
||||
totalBlocks,
|
||||
status,
|
||||
blocksLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
@@ -57,4 +57,4 @@ const IntegrationChipSkeleton: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
IntegrationChip.Skeleton = IntegrationChipSkeleton;
|
||||
IntegrationChip.Skeleton = IntegrationChipSkeleton;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
import { IntegrationBlocks } from "../IntegrationBlocks/IntegrationBlocks";
|
||||
import { PaginatedIntegrationList } from "../PaginatedIntegrationList/PaginatedIntegrationList";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const IntegrationsContent = () => {
|
||||
const { integration } = useBlockMenuContext();
|
||||
|
||||
if (!integration) {
|
||||
return <PaginatedIntegrationList />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
scrollbarStyles,
|
||||
"h-full overflow-y-auto pt-4 transition-all duration-200",
|
||||
)}
|
||||
>
|
||||
<div className="w-full px-4 pb-4">
|
||||
<IntegrationBlocks />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { Plus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { highlightText } from "./helpers";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
@@ -17,8 +17,6 @@ interface IntegrationBlockComponent extends React.FC<Props> {
|
||||
Skeleton: React.FC<{ className?: string }>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const IntegrationBlock: IntegrationBlockComponent = ({
|
||||
title,
|
||||
icon_url,
|
||||
@@ -29,6 +27,7 @@ export const IntegrationBlock: IntegrationBlockComponent = ({
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className={cn(
|
||||
"group flex h-16 w-full min-w-[7.5rem] items-center justify-start gap-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
|
||||
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:cursor-not-allowed",
|
||||
@@ -96,4 +95,4 @@ const IntegrationBlockSkeleton = ({ className }: { className?: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
IntegrationBlock.Skeleton = IntegrationBlockSkeleton;
|
||||
IntegrationBlock.Skeleton = IntegrationBlockSkeleton;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ExternalLink, Loader2, Plus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import Link from "next/link";
|
||||
import { highlightText } from "./helpers";
|
||||
import {
|
||||
ArrowSquareOutIcon,
|
||||
CircleNotchIcon,
|
||||
PlusIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
@@ -89,7 +93,10 @@ export const MarketplaceAgentBlock: MarketplaceAgentBlockComponent = ({
|
||||
<span className="font-sans text-xs leading-5 text-blue-700 underline">
|
||||
Agent page
|
||||
</span>
|
||||
<ExternalLink className="h-4 w-4 text-blue-700" strokeWidth={1} />
|
||||
<ArrowSquareOutIcon
|
||||
className="h-4 w-4 text-blue-700"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,9 +106,9 @@ export const MarketplaceAgentBlock: MarketplaceAgentBlockComponent = ({
|
||||
)}
|
||||
>
|
||||
{!loading ? (
|
||||
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
|
||||
<PlusIcon className="h-5 w-5 text-zinc-50" strokeWidth={2} />
|
||||
) : (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<CircleNotchIcon className="h-5 w-5 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
@@ -132,4 +139,4 @@ const MarketplaceAgentBlockSkeleton: React.FC<{ className?: string }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
MarketplaceAgentBlock.Skeleton = MarketplaceAgentBlockSkeleton;
|
||||
MarketplaceAgentBlock.Skeleton = MarketplaceAgentBlockSkeleton;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import { MarketplaceAgentBlock } from "../MarketplaceAgentBlock";
|
||||
import { useMarketplaceAgentsContent } from "./useMarketplaceAgentsContent";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
export const MarketplaceAgentsContent = () => {
|
||||
const {
|
||||
handleAddStoreAgent,
|
||||
addingAgent,
|
||||
isListStoreAgentsLoading,
|
||||
isListStoreAgentsError,
|
||||
listStoreAgentsError,
|
||||
listStoreAgents,
|
||||
refetchListStoreAgents,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
status,
|
||||
} = useMarketplaceAgentsContent();
|
||||
|
||||
if (isListStoreAgentsLoading) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
scrollbarStyles,
|
||||
"h-full overflow-y-auto pt-4 transition-all duration-200",
|
||||
)}
|
||||
>
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<MarketplaceAgentBlock.Skeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isListStoreAgentsError) {
|
||||
return (
|
||||
<div className="h-full p-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
context="block menu"
|
||||
httpError={{
|
||||
status: status,
|
||||
statusText: "Request failed",
|
||||
message:
|
||||
(listStoreAgentsError?.detail as unknown as string) ||
|
||||
"An error occurred",
|
||||
}}
|
||||
responseError={listStoreAgentsError || undefined}
|
||||
onRetry={() => refetchListStoreAgents()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
className={blockMenuContainerStyle}
|
||||
>
|
||||
{listStoreAgents?.map((agent, index) => (
|
||||
<MarketplaceAgentBlock
|
||||
key={agent.slug + index}
|
||||
slug={agent.slug}
|
||||
title={agent.agent_name}
|
||||
image_url={agent.agent_image}
|
||||
creator_name={agent.creator}
|
||||
number_of_runs={agent.runs}
|
||||
loading={addingAgent === agent.slug}
|
||||
onClick={() =>
|
||||
handleAddStoreAgent({
|
||||
creator_name: agent.creator,
|
||||
slug: agent.slug,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { getGetV2GetBuilderItemCountsQueryKey } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import {
|
||||
getGetV2ListLibraryAgentsQueryKey,
|
||||
usePostV2AddMarketplaceAgent,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import {
|
||||
getV2GetSpecificAgent,
|
||||
useGetV2ListStoreAgentsInfinite,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { StoreAgentsResponse } from "@/lib/autogpt-server-api";
|
||||
import { getQueryClient } from "@/lib/react-query/queryClient";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useMarketplaceAgentsContent = () => {
|
||||
const { toast } = useToast();
|
||||
const [addingAgent, setAddingAgent] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data: listStoreAgents,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isLoading: isListStoreAgentsLoading,
|
||||
isError: isListStoreAgentsError,
|
||||
error: listStoreAgentsError,
|
||||
refetch: refetchListStoreAgents,
|
||||
} = useGetV2ListStoreAgentsInfinite(
|
||||
{
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as StoreAgentsResponse).pagination;
|
||||
const isMore =
|
||||
pagination.current_page * pagination.page_size <
|
||||
pagination.total_items;
|
||||
|
||||
return isMore ? pagination.current_page + 1 : undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const allAgents =
|
||||
listStoreAgents?.pages?.flatMap((page) => {
|
||||
const response = page.data as StoreAgentsResponse;
|
||||
return response.agents;
|
||||
}) ?? [];
|
||||
|
||||
const status = listStoreAgents?.pages[0]?.status;
|
||||
|
||||
const { mutate: addMarketplaceAgent } = usePostV2AddMarketplaceAgent({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
queryClient.refetchQueries({
|
||||
queryKey: getGetV2GetBuilderItemCountsQueryKey(),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddStoreAgent = async ({
|
||||
creator_name,
|
||||
slug,
|
||||
}: {
|
||||
creator_name: string;
|
||||
slug: string;
|
||||
}) => {
|
||||
try {
|
||||
setAddingAgent(slug);
|
||||
const { data: agent, status } = await getV2GetSpecificAgent(
|
||||
creator_name,
|
||||
slug,
|
||||
);
|
||||
if (status !== 200) {
|
||||
Sentry.captureException("Store listing version not found");
|
||||
throw new Error("Store listing version not found");
|
||||
}
|
||||
|
||||
addMarketplaceAgent({
|
||||
data: {
|
||||
store_listing_version_id: agent?.store_listing_version_id,
|
||||
},
|
||||
});
|
||||
|
||||
// Need a way to convert the library agent into block
|
||||
// then add the block in builder
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add agent to library",
|
||||
});
|
||||
} finally {
|
||||
setAddingAgent(null);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleAddStoreAgent,
|
||||
listStoreAgents: allAgents,
|
||||
status,
|
||||
addingAgent,
|
||||
isListStoreAgentsLoading,
|
||||
isListStoreAgentsError,
|
||||
listStoreAgentsError,
|
||||
refetchListStoreAgents,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
};
|
||||
};
|
||||
@@ -30,11 +30,11 @@ export const MenuItem: React.FC<Props> = ({
|
||||
<span className="truncate font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
{name}
|
||||
</span>
|
||||
{number && (
|
||||
{number !== undefined && (
|
||||
<span className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-600">
|
||||
{number > 100 ? "100+" : number}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { UGCAgentBlock } from "../UGCAgentBlock";
|
||||
import { useMyAgentsContent } from "./useMyAgentsContent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
export const MyAgentsContent = () => {
|
||||
const {
|
||||
allAgents,
|
||||
agentLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
status,
|
||||
refetch,
|
||||
} = useMyAgentsContent();
|
||||
|
||||
if (agentLoading) {
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<UGCAgentBlock.Skeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="h-full p-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
context="block menu"
|
||||
responseError={error || undefined}
|
||||
httpError={{
|
||||
status: status,
|
||||
statusText: "Request failed",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
className={blockMenuContainerStyle}
|
||||
>
|
||||
{allAgents.map((agent) => (
|
||||
<UGCAgentBlock
|
||||
key={agent.id}
|
||||
title={agent.name}
|
||||
edited_time={agent.updated_at}
|
||||
version={agent.graph_version}
|
||||
image_url={agent.image_url}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
|
||||
export const useMyAgentsContent = () => {
|
||||
const {
|
||||
data: agents,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
isLoading: agentLoading,
|
||||
refetch,
|
||||
error,
|
||||
} = useGetV2ListLibraryAgentsInfinite(
|
||||
{
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
|
||||
const isMore =
|
||||
pagination.current_page * pagination.page_size <
|
||||
pagination.total_items;
|
||||
|
||||
return isMore ? pagination.current_page + 1 : undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const allAgents =
|
||||
agents?.pages?.flatMap((page) => {
|
||||
const response = page.data as LibraryAgentResponse;
|
||||
return response.agents;
|
||||
}) ?? [];
|
||||
|
||||
const status = agents?.pages[0]?.status;
|
||||
|
||||
return {
|
||||
allAgents,
|
||||
agentLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
refetch,
|
||||
error,
|
||||
status,
|
||||
};
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export const NewControlPanel = ({
|
||||
className,
|
||||
}: ControlPanelProps) => {
|
||||
const isGraphSearchEnabled = useGetFlag(Flag.GRAPH_SEARCH);
|
||||
|
||||
|
||||
const {
|
||||
blockMenuSelected,
|
||||
setBlockMenuSelected,
|
||||
@@ -71,14 +71,14 @@ export const NewControlPanel = ({
|
||||
disabled: !history.canRedo(),
|
||||
},
|
||||
],
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"absolute left-4 top-24 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
|
||||
|
||||
@@ -4,11 +4,14 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export interface NewControlPanelProps {
|
||||
flowExecutionID: GraphExecutionID | undefined;
|
||||
visualizeBeads: "no" | "static" | "animate";
|
||||
flowExecutionID: GraphExecutionID | undefined;
|
||||
visualizeBeads: "no" | "static" | "animate";
|
||||
}
|
||||
|
||||
export const useNewControlPanel = ({flowExecutionID, visualizeBeads}: NewControlPanelProps) => {
|
||||
export const useNewControlPanel = ({
|
||||
flowExecutionID,
|
||||
visualizeBeads,
|
||||
}: NewControlPanelProps) => {
|
||||
const [blockMenuSelected, setBlockMenuSelected] = useState<
|
||||
"save" | "block" | "search" | ""
|
||||
>("");
|
||||
@@ -16,8 +19,23 @@ export const useNewControlPanel = ({flowExecutionID, visualizeBeads}: NewControl
|
||||
const _graphVersion = query.get("flowVersion");
|
||||
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined;
|
||||
|
||||
const flowID = query.get("flowID") as GraphID | null ?? undefined;
|
||||
const {agentDescription, setAgentDescription, saveAgent, agentName, setAgentName, savedAgent, isSaving, isRunning, isStopping} = useAgentGraph(flowID, graphVersion, flowExecutionID, visualizeBeads !== "no")
|
||||
const flowID = (query.get("flowID") as GraphID | null) ?? undefined;
|
||||
const {
|
||||
agentDescription,
|
||||
setAgentDescription,
|
||||
saveAgent,
|
||||
agentName,
|
||||
setAgentName,
|
||||
savedAgent,
|
||||
isSaving,
|
||||
isRunning,
|
||||
isStopping,
|
||||
} = useAgentGraph(
|
||||
flowID,
|
||||
graphVersion,
|
||||
flowExecutionID,
|
||||
visualizeBeads !== "no",
|
||||
);
|
||||
|
||||
return {
|
||||
blockMenuSelected,
|
||||
@@ -31,5 +49,5 @@ export const useNewControlPanel = ({flowExecutionID, visualizeBeads}: NewControl
|
||||
isSaving,
|
||||
isRunning,
|
||||
isStopping,
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@ export const NoSearchResult = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { BlocksList } from "../BlockList/BlockList";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { usePaginatedBlocks } from "./usePaginatedBlocks";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
interface PaginatedBlocksContentProps {
|
||||
type?: "all" | "input" | "action" | "output" | null;
|
||||
}
|
||||
|
||||
export const PaginatedBlocksContent: React.FC<PaginatedBlocksContentProps> = ({
|
||||
type,
|
||||
}) => {
|
||||
const {
|
||||
allBlocks: blocks,
|
||||
status,
|
||||
blocksLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
refetch,
|
||||
} = usePaginatedBlocks({
|
||||
type,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full px-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
httpError={{
|
||||
status: status,
|
||||
statusText: "Request failed",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
responseError={error || undefined}
|
||||
context="block menu"
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
className={blockMenuContainerStyle}
|
||||
>
|
||||
<BlocksList blocks={blocks} loading={blocksLoading} />
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useGetV2GetBuilderBlocksInfinite } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { BlockResponse } from "@/app/api/__generated__/models/blockResponse";
|
||||
|
||||
interface UsePaginatedBlocksProps {
|
||||
type?: "all" | "input" | "action" | "output" | null;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
export const usePaginatedBlocks = ({ type }: UsePaginatedBlocksProps) => {
|
||||
const {
|
||||
data: blocks,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isLoading: blocksLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetV2GetBuilderBlocksInfinite(
|
||||
{
|
||||
page: 1,
|
||||
page_size: PAGE_SIZE,
|
||||
type,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as BlockResponse).pagination;
|
||||
const isMore =
|
||||
pagination.current_page * pagination.page_size <
|
||||
pagination.total_items;
|
||||
|
||||
return isMore ? pagination.current_page + 1 : undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const allBlocks =
|
||||
blocks?.pages?.flatMap((page) => {
|
||||
const response = page.data as BlockResponse;
|
||||
return response.blocks;
|
||||
}) ?? [];
|
||||
|
||||
const status = blocks?.pages[0]?.status;
|
||||
|
||||
return {
|
||||
allBlocks,
|
||||
status,
|
||||
blocksLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { Integration } from "../Integration";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
|
||||
import { usePaginatedIntegrationList } from "./usePaginatedIntegrationList";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
export const PaginatedIntegrationList = () => {
|
||||
const { setIntegration } = useBlockMenuContext();
|
||||
const {
|
||||
allProviders: providers,
|
||||
providersLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
status,
|
||||
refetch,
|
||||
} = usePaginatedIntegrationList();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full px-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={error || undefined}
|
||||
context="block menu"
|
||||
httpError={{
|
||||
status: status,
|
||||
statusText: "Request failed",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (providersLoading && providers.length === 0) {
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
{Array.from({ length: 6 }).map((_, integrationIndex) => (
|
||||
<Integration.Skeleton key={integrationIndex} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
className={blockMenuContainerStyle}
|
||||
>
|
||||
{providers.map((integration, index) => (
|
||||
<Integration
|
||||
key={integration.name + index}
|
||||
title={integration.name}
|
||||
icon_url={`/integrations/${integration.name}.png`}
|
||||
description={integration.description}
|
||||
number_of_blocks={integration.integration_count}
|
||||
onClick={() => setIntegration(integration.name)}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useGetV2GetBuilderIntegrationProvidersInfinite } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { ProviderResponse } from "@/app/api/__generated__/models/providerResponse";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export const usePaginatedIntegrationList = () => {
|
||||
const {
|
||||
data: providers,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isLoading: providersLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetV2GetBuilderIntegrationProvidersInfinite(
|
||||
{
|
||||
page: 1,
|
||||
page_size: PAGE_SIZE,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
getNextPageParam: (lastPage: any) => {
|
||||
const pagination = (lastPage.data as ProviderResponse).pagination;
|
||||
const isMore =
|
||||
pagination.current_page * pagination.page_size <
|
||||
pagination.total_items;
|
||||
|
||||
return isMore ? pagination.current_page + 1 : undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const allProviders =
|
||||
providers?.pages?.flatMap((page: any) => {
|
||||
const response = page.data as ProviderResponse;
|
||||
return response.providers;
|
||||
}) ?? [];
|
||||
|
||||
const status = providers?.pages[0]?.status;
|
||||
|
||||
return {
|
||||
allProviders,
|
||||
providersLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
error,
|
||||
refetch,
|
||||
status,
|
||||
};
|
||||
};
|
||||
@@ -41,7 +41,6 @@ export const NewSaveControl = ({
|
||||
setBlockMenuSelected,
|
||||
pinSavePopover,
|
||||
}: SaveControlProps) => {
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave();
|
||||
}, [onSave]);
|
||||
@@ -51,8 +50,8 @@ export const NewSaveControl = ({
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
handleSave();
|
||||
event.preventDefault();
|
||||
handleSave();
|
||||
toast({
|
||||
duration: 2000,
|
||||
title: "All changes saved successfully!",
|
||||
@@ -155,4 +154,4 @@ export const NewSaveControl = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -44,4 +44,4 @@ const SearchHistoryChipSkeleton: React.FC<{ className?: string }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
SearchHistoryChip.Skeleton = SearchHistoryChipSkeleton;
|
||||
SearchHistoryChip.Skeleton = SearchHistoryChipSkeleton;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import { IntegrationChip } from "../IntegrationChip";
|
||||
import { Block } from "../Block";
|
||||
import { DefaultStateType, useBlockMenuContext } from "../block-menu-provider";
|
||||
import { useSuggestionContent } from "./useSuggestionContent";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { blockMenuContainerStyle } from "../style";
|
||||
|
||||
export const SuggestionContent = () => {
|
||||
const { setIntegration, setDefaultState } = useBlockMenuContext();
|
||||
const { data, isLoading, isError, error, refetch } = useSuggestionContent();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="h-full p-4">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={error || undefined}
|
||||
httpError={{
|
||||
status: data?.status,
|
||||
statusText: "Request failed",
|
||||
message: (error?.detail as string) || "An error occurred",
|
||||
}}
|
||||
context="block menu"
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const suggestions = data?.suggestions;
|
||||
|
||||
return (
|
||||
<div className={blockMenuContainerStyle}>
|
||||
<div className="w-full space-y-6 pb-4">
|
||||
{/* Integrations */}
|
||||
<div className="space-y-2.5 px-4">
|
||||
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
Integrations
|
||||
</p>
|
||||
<div className="grid grid-cols-3 grid-rows-2 gap-2">
|
||||
{!isLoading && suggestions
|
||||
? suggestions.providers.map((provider, index) => (
|
||||
<IntegrationChip
|
||||
key={`integration-${index}`}
|
||||
icon_url={`/integrations/${provider}.png`}
|
||||
name={provider}
|
||||
onClick={() => {
|
||||
setDefaultState(DefaultStateType.INTEGRATIONS);
|
||||
setIntegration(provider);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: Array(6)
|
||||
.fill(0)
|
||||
.map((_, index) => (
|
||||
<IntegrationChip.Skeleton
|
||||
key={`integration-skeleton-${index}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top blocks */}
|
||||
<div className="space-y-2.5 px-4">
|
||||
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
Top blocks
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{!isLoading && suggestions
|
||||
? suggestions.top_blocks.map((block, index) => (
|
||||
<Block
|
||||
key={`block-${index}`}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
/>
|
||||
))
|
||||
: Array(3)
|
||||
.fill(0)
|
||||
.map((_, index) => (
|
||||
<Block.Skeleton key={`block-skeleton-${index}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useGetV2GetBuilderSuggestions } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import { SuggestionsResponse } from "@/app/api/__generated__/models/suggestionsResponse";
|
||||
|
||||
export const useSuggestionContent = () => {
|
||||
const { data, isLoading, isError, error, refetch } =
|
||||
useGetV2GetBuilderSuggestions({
|
||||
query: {
|
||||
select: (x) => {
|
||||
return {
|
||||
suggestions: x.data as SuggestionsResponse,
|
||||
status: x.status,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { data, isLoading, isError, error, refetch };
|
||||
};
|
||||
@@ -11,7 +11,7 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
title?: string;
|
||||
edited_time?: Date;
|
||||
version?: number;
|
||||
image_url?: string;
|
||||
image_url: string | null;
|
||||
highlightedText?: string;
|
||||
}
|
||||
|
||||
@@ -114,4 +114,4 @@ const UGCAgentBlockSkeleton: React.FC<{ className?: string }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
UGCAgentBlock.Skeleton = UGCAgentBlockSkeleton;
|
||||
UGCAgentBlock.Skeleton = UGCAgentBlockSkeleton;
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
export type DefaultStateType =
|
||||
| "suggestion"
|
||||
| "all_blocks"
|
||||
| "input_blocks"
|
||||
| "action_blocks"
|
||||
| "output_blocks"
|
||||
| "integrations"
|
||||
| "marketplace_agents"
|
||||
| "my_agents";
|
||||
|
||||
export enum DefaultStateType {
|
||||
SUGGESTION = "suggestion",
|
||||
ALL_BLOCKS = "all_blocks",
|
||||
INPUT_BLOCKS = "input_blocks",
|
||||
ACTION_BLOCKS = "action_blocks",
|
||||
OUTPUT_BLOCKS = "output_blocks",
|
||||
INTEGRATIONS = "integrations",
|
||||
MARKETPLACE_AGENTS = "marketplace_agents",
|
||||
MY_AGENTS = "my_agents",
|
||||
}
|
||||
|
||||
interface BlockMenuContextType {
|
||||
searchQuery: string;
|
||||
@@ -20,7 +20,9 @@ interface BlockMenuContextType {
|
||||
setSearchId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
defaultState: DefaultStateType;
|
||||
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
|
||||
}
|
||||
integration: string | undefined;
|
||||
setIntegration: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
export const BlockMenuContext = createContext<BlockMenuContextType>(
|
||||
{} as BlockMenuContextType,
|
||||
@@ -35,7 +37,10 @@ export function BlockMenuStateProvider({
|
||||
}: BlockMenuStateProviderProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchId, setSearchId] = useState<string | undefined>(undefined);
|
||||
const [defaultState, setDefaultState] = useState<DefaultStateType>("suggestion");
|
||||
const [defaultState, setDefaultState] = useState<DefaultStateType>(
|
||||
DefaultStateType.SUGGESTION,
|
||||
);
|
||||
const [integration, setIntegration] = useState<string | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<BlockMenuContext.Provider
|
||||
@@ -46,6 +51,8 @@ export function BlockMenuStateProvider({
|
||||
setSearchId,
|
||||
defaultState,
|
||||
setDefaultState,
|
||||
integration,
|
||||
setIntegration,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -61,4 +68,4 @@ export function useBlockMenuContext(): BlockMenuContextType {
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
export const highlightText = (
|
||||
text: string | undefined,
|
||||
highlight: string | undefined,
|
||||
) => {
|
||||
if (!text || !highlight) return text;
|
||||
|
||||
function escapeRegExp(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const escaped = escapeRegExp(highlight);
|
||||
const parts = text.split(new RegExp(`(${escaped})`, "gi"));
|
||||
return parts.map((part, i) =>
|
||||
part.toLowerCase() === highlight?.toLowerCase() ? (
|
||||
<mark key={i} className="bg-transparent font-bold">
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
};
|
||||
text: string | undefined,
|
||||
highlight: string | undefined,
|
||||
) => {
|
||||
if (!text || !highlight) return text;
|
||||
|
||||
function escapeRegExp(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const escaped = escapeRegExp(highlight);
|
||||
const parts = text.split(new RegExp(`(${escaped})`, "gi"));
|
||||
return parts.map((part, i) =>
|
||||
part.toLowerCase() === highlight?.toLowerCase() ? (
|
||||
<mark key={i} className="bg-transparent font-bold">
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const blockMenuContainerStyle =
|
||||
"scrollbar-thin scrollbar-thumb-zinc-300 scrollbar-track-transparent w-full px-4 pb-4 space-y-3 h-full overflow-y-auto pt-4 transition-all duration-200";
|
||||