Compare commits

...

92 Commits

Author SHA1 Message Date
Krzysztof Czerwinski
9c07206725 Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-06-06 14:43:17 +02:00
Krzysztof Czerwinski
4bd3447301 Cleanup and comments 2025-06-06 14:39:13 +02:00
Abhimanyu Yadav
8adc9f967d Remove commented TODO and clean up code formatting 2025-06-06 17:11:40 +05:30
Abhimanyu Yadav
349b70c4bc Remove unused imports and cleanup effects 2025-06-06 17:07:59 +05:30
Abhimanyu Yadav
9ecfa1e1f1 Add scrolling and fixed footer to filter sheet panel 2025-06-06 17:03:24 +05:30
Krzysztof Czerwinski
4e17f9c49e Include block costs in get_blocks 2025-06-06 13:05:44 +02:00
Krzysztof Czerwinski
31fdeeb706 Make agent_name optional 2025-06-06 13:03:32 +02:00
Abhimanyu Yadav
e42b24c029 Add hasLocalActiveFilters for applying filter state 2025-06-06 11:10:47 +05:30
Abhimanyu Yadav
2d52a57a21 Fix "Agent page" link propagation in menu block 2025-06-06 10:53:55 +05:30
Krzysztof Czerwinski
f45123f6b6 Fix Agent Executor block name 2025-06-05 17:00:40 +02:00
Abhimanyu Yadav
d524518f41 Update pnpm-lock.yaml 2025-06-05 18:32:07 +05:30
abhi1992002
81d1b28d92 Merge remote-tracking branch 'upstream/dev' into redesigning-block-menu 2025-06-05 18:30:19 +05:30
Abhimanyu Yadav
4e4e754ac1 Merge branch 'backend-temp' into redesigning-block-menu 2025-06-05 17:00:08 +05:30
Krzysztof Czerwinski
e409d7aa34 Fixes 2025-06-04 14:58:49 +02:00
Abhimanyu Yadav
8312a339c2 cleaning up frontend code 2025-06-04 11:22:46 +05:30
Abhimanyu Yadav
5b45d246ef fix blockType utils 2025-06-04 10:46:45 +05:30
Krzysztof Czerwinski
5c7c7ca874 Suggested blocks 2025-06-03 19:27:02 +02:00
Krzysztof Czerwinski
c93c5e35ba Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-06-03 11:45:34 +02:00
Abhimanyu Yadav
ce989b1bf7 remove providers from filter list and add support of ai blocks in search
list]
2025-06-03 10:46:25 +05:30
Abhimanyu Yadav
c1c919b88b Merge branch 'backend-temp' into redesigning-block-menu 2025-06-03 10:22:01 +05:30
Krzysztof Czerwinski
21a91fe9fd Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-06-02 15:07:16 +02:00
Krzysztof Czerwinski
b2f3d8c1f2 Search model names 2025-06-02 15:06:54 +02:00
Krzysztof Czerwinski
46ab2e3b20 Remove providers filter from search 2025-06-02 10:13:05 +02:00
Abhimanyu Yadav
5b40700299 fetching creator list from searchList
Moves the `getBlockType` function from the SearchList component to the
`utils.ts` file to make it more reusable. Also removes the unused
`creators` state and `setCreators` function from the
BlockMenuContext and instead calculates the creators list dynamically
within the FilterSheet component based on the available search data.
2025-06-02 13:07:35 +05:30
Abhimanyu Yadav
1a97020eeb fix marketplace agent block and libray agent block in searchList 2025-06-02 12:50:07 +05:30
Abhimanyu Yadav
39d03f2090 Add Marketplace Agents to builder
Adds functionality to add Marketplace agents to the user's library and then to builder.
Includes a loading indicator while the agent is being added.
Refactors agent-to-block conversion into a utility function.
2025-06-02 12:32:13 +05:30
Abhimanyu Yadav
8088d294f4 Add Agent Blocks to Flow
This commit adds the ability to add Agent blocks to the
flow.  Clicking on an agent in the My Agents menu will add
it to the flow.  The block includes the necessary
information such as input/output schemas.
2025-06-02 12:03:51 +05:30
Abhimanyu Yadav
31266949ed Clears all filters when the search input is cleared and redesign filter based on new design. 2025-06-02 11:34:01 +05:30
abhi1992002
f4eb00a6ad Fetch Block Counts in Block Menu
Adds API calls to fetch block counts for each category
in the block menu and displays them next to the category
name.  This replaces the hardcoded numbers previously
displayed.
2025-06-02 10:50:26 +05:30
Abhimanyu Yadav
f75cc0dd11 Merge branch 'dev' into redesigning-block-menu 2025-06-02 10:34:16 +05:30
Krzysztof Czerwinski
21b612625f Format frontend 2025-05-31 13:42:32 +02:00
Krzysztof Czerwinski
eec0d276d5 Add output_schema to LibraryAgent 2025-05-31 13:42:07 +02:00
Krzysztof Czerwinski
c6941e7f6e Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-05-31 12:49:36 +02:00
Abhimanyu Yadav
325684a10f remove recent searches from suggestionContent and done some cleanup as
well
2025-05-30 17:33:44 +05:30
Abhimanyu Yadav
cf057cbbda fixed pagination problem in default menus in block menu 2025-05-30 17:26:59 +05:30
Abhimanyu Yadav
f3a7be1fd3 add highlighted description while searching 2025-05-30 12:06:36 +05:30
Abhimanyu Yadav
97bcb0f95e fix searchlist pagination 2025-05-30 11:11:40 +05:30
Abhimanyu Yadav
dd71d65706 adding beautify String in integration chips 2025-05-30 10:57:08 +05:30
Abhimanyu Yadav
2b2d26bcde remove items expanding when selecting menus 2025-05-30 10:54:56 +05:30
Abhimanyu Yadav
67f6f43e1b fix error state layout in input/output/action blocks list 2025-05-30 10:43:03 +05:30
Abhimanyu Yadav
a3409c9578 fix MarketplaceAgentBlock layout 2025-05-30 10:29:53 +05:30
Abhimanyu Yadav
7f82457ea4 add external agent link to marketplace agent block 2025-05-30 10:25:40 +05:30
Abhimanyu Yadav
a5c0fabc00 fix design of clear button in searchMenuBar 2025-05-30 10:12:09 +05:30
Krzysztof Czerwinski
09dba93a4a Add counts endpoint 2025-05-29 16:17:33 +02:00
Krzysztof Czerwinski
ea2cd3e7bf Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-05-29 13:16:18 +02:00
Abhimanyu Yadav
d3d0ccf732 fix menu item hover state and add a clear button at the end of searchbar 2025-05-29 11:09:55 +05:30
Abhimanyu Yadav
d8d5d6ec0c make hover state correct on all reusable compoents in block menu 2025-05-29 11:01:09 +05:30
Abhimanyu Yadav
f45b09c0b5 fix hover state and heading text in suggestion content page 2025-05-29 10:57:08 +05:30
Abhimanyu Yadav
1e89b6d3a4 add beautifyString in block, integration and integration block 2025-05-29 10:31:10 +05:30
Abhimanyu Yadav
950a85e179 fix image sizes warning with fill 2025-05-28 17:40:35 +05:30
Abhimanyu Yadav
c5e3148145 add better error handling in all components 2025-05-28 17:27:07 +05:30
Abhimanyu Yadav
a135ba3f0b refactor addBlock implementation in flow.tsx 2025-05-28 15:58:31 +05:30
Abhimanyu Yadav
fe95e27226 only show scroller when hovering 2025-05-28 15:48:28 +05:30
Abhimanyu Yadav
711ca10cc9 add relative time in my_agent block using react-timeago library 2025-05-28 15:29:34 +05:30
Abhimanyu Yadav
1346d8230c add 500ms debouncer on searchbar 2025-05-28 15:19:56 +05:30
Abhimanyu Yadav
07c84a4757 add categories filter in search 2025-05-28 13:57:39 +05:30
Abhimanyu Yadav
596824c1e7 add pagination on search list 2025-05-28 13:28:32 +05:30
Abhimanyu Yadav
79afa6db99 add search functioanlity in block menu 2025-05-28 12:15:38 +05:30
Abhimanyu Yadav
e034c16f31 add pagination in all components in default state 2025-05-26 21:13:51 +05:30
Abhimanyu Yadav
9012eff1ac add basic data fetching in all default state components 2025-05-26 10:27:15 +05:30
Abhimanyu Yadav
0361ea4aa4 connection integration list and blocks 2025-05-26 00:25:30 +05:30
Abhimanyu Yadav
6f1c522ea3 add some images and connect suggestion content frontend with backend 2025-05-25 23:09:22 +05:30
Krzysztof Czerwinski
2d654bf64b Update frontend types and api client 2025-05-25 15:12:01 +02:00
Krzysztof Czerwinski
bb69e32fee Update backend 2025-05-25 15:11:29 +02:00
Krzysztof Czerwinski
1be830835b Update signatures, disable providers 2025-05-23 17:22:35 +02:00
Krzysztof Czerwinski
a2a4d546f7 Merge branch 'redesigning-block-menu' into kpczerwinski/secrt-1320-backend-update 2025-05-23 16:51:53 +02:00
Krzysztof Czerwinski
3053a7bd06 Add types and function on the frontend 2025-05-23 16:50:52 +02:00
Krzysztof Czerwinski
bbf4108136 Add builder router and get_blocks endpoint 2025-05-23 16:50:05 +02:00
Krzysztof Czerwinski
95387bcf78 Add model and functions 2025-05-23 16:48:34 +02:00
Abhimanyu Yadav
e1fc56e6f3 fix small optimisation and DX issue 2025-05-21 18:10:29 +05:30
Abhimanyu Yadav
2a06956802 fix max width in sidebar 2025-05-20 17:01:03 +05:30
Abhimanyu Yadav
32231ff80f Implement search text highlighting in Block components, add transitions
to FilterChip, and create NoSearchResult component for empty searches. Move
SearchItem types to provider context for better access.
2025-05-20 15:31:02 +05:30
Abhimanyu Yadav
d0b23c085f add context api for block menu 2025-05-20 11:58:45 +05:30
Abhimanyu Yadav
e718d3d3d8 fix filter sheets 2025-05-20 11:25:46 +05:30
Abhimanyu Yadav
1971a62684 fix checkbox tick design 2025-05-20 10:38:51 +05:30
Abhimanyu Yadav
e125b5923c fix width of left sidebar 2025-05-20 10:30:18 +05:30
Abhimanyu Yadav
c6942e4e6f prevent layout shift when clicking result elements with border 2025-05-20 10:19:02 +05:30
Abhimanyu Yadav
c9e421a219 Merge branch 'dev' into redesigning-block-menu 2025-05-19 22:27:23 +05:30
Abhimanyu Yadav
7868373897 fix comments 2025-05-19 17:06:17 +05:30
Abhimanyu Yadav
f1c8399e0e fix recent searches onClick 2025-05-19 16:55:59 +05:30
Abhimanyu Yadav
97ba69ef1c fix lint 2025-05-19 16:35:55 +05:30
Abhimanyu Yadav
773e1488bf add filter sheet 2025-05-19 16:34:26 +05:30
Abhimanyu Yadav
4273be59ba fix format 2025-05-19 15:37:13 +05:30
Abhimanyu Yadav
06e524788a fix format 2025-05-18 21:10:26 +05:30
Abhimanyu Yadav
bc08012771 add search list in block menu 2025-05-18 21:10:19 +05:30
Abhimanyu Yadav
4af0aedebd fix format 2025-05-18 17:16:45 +05:30
Abhimanyu Yadav
d22464a75e Add skeleton components and loading states 2025-05-18 17:16:08 +05:30
Abhimanyu Yadav
82e3a485f0 complete frontend design for default state 2025-05-18 10:19:25 +05:30
Abhimanyu Yadav
8165ad5879 fix scrollbar in default content 2025-05-18 08:52:20 +05:30
Abhimanyu Yadav
451284de76 Add tailwind-scrollbar-hide and implement block menu UI
The commit adds a new block menu UI component with sidebar navigation,
integration chips, and scrollable content areas. It includes tailwind-
scrollbar-hide for better UI experience and custom CSS for scroll
containers. The implementation features different content sections
for blocks categorized by type (input, action, output) and supports
search functionality.
2025-05-17 21:18:08 +05:30
Abhimanyu Yadav
1d8c7c5e1a Merge branch 'dev' into redesigning-block-menu 2025-05-17 00:13:52 +05:30
Abhimanyu Yadav
34be6a3379 creating small ui reusable component 2025-05-17 00:01:40 +05:30
85 changed files with 4191 additions and 119 deletions

View File

@@ -22,6 +22,9 @@ class AgentExecutorBlock(Block):
user_id: str = SchemaField(description="User ID")
graph_id: str = SchemaField(description="Graph ID")
graph_version: int = SchemaField(description="Graph Version")
agent_name: Optional[str] = SchemaField(
default=None, description="Name to display in the Builder UI"
)
inputs: BlockInput = SchemaField(description="Input data for the graph")
input_schema: dict = SchemaField(description="Input schema for the graph")

View File

@@ -74,6 +74,15 @@ class Pagination(pydantic.BaseModel):
description="Number of items per page.", examples=[25]
)
@staticmethod
def empty() -> "Pagination":
return Pagination(
total_items=0,
total_pages=0,
current_page=0,
page_size=0,
)
class RequestTopUp(pydantic.BaseModel):
credit_amount: int

View File

@@ -21,6 +21,8 @@ import backend.server.routers.postmark.postmark
import backend.server.routers.v1
import backend.server.v2.admin.credit_admin_routes
import backend.server.v2.admin.store_admin_routes
import backend.server.v2.builder
import backend.server.v2.builder.routes
import backend.server.v2.library.db
import backend.server.v2.library.model
import backend.server.v2.library.routes
@@ -104,6 +106,9 @@ app.include_router(backend.server.routers.v1.v1_router, tags=["v1"], prefix="/ap
app.include_router(
backend.server.v2.store.routes.router, tags=["v2"], prefix="/api/store"
)
app.include_router(
backend.server.v2.builder.routes.router, tags=["v2"], prefix="/api/builder"
)
app.include_router(
backend.server.v2.admin.store_admin_routes.router,
tags=["v2", "admin"],

View File

@@ -0,0 +1,370 @@
import functools
import logging
import prisma
import backend.data.block
import backend.server.model as server_model
from backend.blocks import load_all_blocks
from backend.blocks.llm import LlmModel
from backend.data.block import Block, BlockCategory, BlockSchema
from backend.data.credit import get_block_costs
from backend.integrations.providers import ProviderName
from backend.server.v2.builder.model import (
BlockCategoryResponse,
BlockData,
BlockResponse,
BlockType,
CountResponse,
Provider,
ProviderResponse,
SearchBlocksResponse,
)
logger = logging.getLogger(__name__)
llm_models = [name.name.lower().replace("_", " ") for name in LlmModel]
_static_counts_cache: dict | None = None
_suggested_blocks: list[BlockData] | None = None
def get_block_categories(category_blocks: int = 3) -> list[BlockCategoryResponse]:
categories: dict[BlockCategory, BlockCategoryResponse] = {}
for block_type in load_all_blocks().values():
block: Block[BlockSchema, BlockSchema] = block_type()
# Skip disabled blocks
if block.disabled:
continue
# Skip blocks that don't have categories (all should have at least one)
if not block.categories:
continue
# Add block to the categories
for category in block.categories:
if category not in categories:
categories[category] = BlockCategoryResponse(
name=category.name.lower(),
total_blocks=0,
blocks=[],
)
categories[category].total_blocks += 1
# Append if the category has less than the specified number of blocks
if len(categories[category].blocks) < category_blocks:
categories[category].blocks.append(block.to_dict())
# Sort categories by name
return sorted(categories.values(), key=lambda x: x.name)
def get_blocks(
*,
category: str | None = None,
type: BlockType | None = None,
provider: ProviderName | None = None,
page: int = 1,
page_size: int = 50,
) -> BlockResponse:
"""
Get blocks based on either category, type or provider.
Providing nothing fetches all block types.
"""
# Only one of category, type, or provider can be specified
if (category and type) or (category and provider) or (type and provider):
raise ValueError("Only one of category, type, or provider can be specified")
blocks: list[Block[BlockSchema, BlockSchema]] = []
skip = (page - 1) * page_size
take = page_size
total = 0
for block_type in load_all_blocks().values():
block: Block[BlockSchema, BlockSchema] = block_type()
# Skip disabled blocks
if block.disabled:
continue
# Skip blocks that don't match the category
if category and category not in {c.name.lower() for c in block.categories}:
continue
# Skip blocks that don't match the type
if (
(type == "input" and block.block_type.value != "Input")
or (type == "output" and block.block_type.value != "Output")
or (type == "action" and block.block_type.value in ("Input", "Output"))
):
continue
# Skip blocks that don't match the provider
if provider:
credentials_info = block.input_schema.get_credentials_fields_info().values()
if not any(provider in info.provider for info in credentials_info):
continue
total += 1
if skip > 0:
skip -= 1
continue
if take > 0:
take -= 1
blocks.append(block)
costs = get_block_costs()
return BlockResponse(
blocks=[{**b.to_dict(), "costs": costs.get(b.id, [])} for b in blocks],
pagination=server_model.Pagination(
total_items=total,
total_pages=total // page_size + (1 if total % page_size > 0 else 0),
current_page=page,
page_size=page_size,
),
)
def search_blocks(
include_blocks: bool = True,
include_integrations: bool = True,
query: str = "",
page: int = 1,
page_size: int = 50,
) -> SearchBlocksResponse:
"""
Get blocks based on the filter and query.
`providers` only applies for `integrations` filter.
"""
blocks: list[Block[BlockSchema, BlockSchema]] = []
query = query.lower()
total = 0
skip = (page - 1) * page_size
take = page_size
block_count = 0
integration_count = 0
for block_type in load_all_blocks().values():
block: Block[BlockSchema, BlockSchema] = block_type()
# Skip disabled blocks
if block.disabled:
continue
# Skip blocks that don't match the query
if (
query not in block.name.lower()
and query not in block.description.lower()
and not _matches_llm_model(block.input_schema, query)
):
continue
keep = False
credentials = list(block.input_schema.get_credentials_fields().values())
if include_integrations and len(credentials) > 0:
keep = True
integration_count += 1
if include_blocks and len(credentials) == 0:
keep = True
block_count += 1
if not keep:
continue
total += 1
if skip > 0:
skip -= 1
continue
if take > 0:
take -= 1
blocks.append(block)
costs = get_block_costs()
return SearchBlocksResponse(
blocks=BlockResponse(
blocks=[{**b.to_dict(), "costs": costs.get(b.id, [])} for b in blocks],
pagination=server_model.Pagination(
total_items=total,
total_pages=total // page_size + (1 if total % page_size > 0 else 0),
current_page=page,
page_size=page_size,
),
),
total_block_count=block_count,
total_integration_count=integration_count,
)
def get_providers(
query: str = "",
page: int = 1,
page_size: int = 50,
) -> ProviderResponse:
providers = []
query = query.lower()
skip = (page - 1) * page_size
take = page_size
all_providers = _get_all_providers()
for provider in all_providers.values():
if (
query not in provider.name.value.lower()
and query not in provider.description.lower()
):
continue
if skip > 0:
skip -= 1
continue
if take > 0:
take -= 1
providers.append(provider)
total = len(all_providers)
return ProviderResponse(
providers=providers,
pagination=server_model.Pagination(
total_items=total,
total_pages=total // page_size + (1 if total % page_size > 0 else 0),
current_page=page,
page_size=page_size,
),
)
async def get_counts(user_id: str) -> CountResponse:
my_agents = await prisma.models.LibraryAgent.prisma().count(
where={
"userId": user_id,
"isDeleted": False,
"isArchived": False,
}
)
counts = await _get_static_counts()
return CountResponse(
my_agents=my_agents,
**counts,
)
async def _get_static_counts():
"""
Get counts of blocks, integrations, and marketplace agents.
This is cached to avoid unnecessary database queries and calculations.
Can't use functools.cache here because the function is async.
"""
global _static_counts_cache
if _static_counts_cache is not None:
return _static_counts_cache
all_blocks = 0
input_blocks = 0
action_blocks = 0
output_blocks = 0
integrations = 0
for block_type in load_all_blocks().values():
block: Block[BlockSchema, BlockSchema] = block_type()
if block.disabled:
continue
all_blocks += 1
if block.block_type.value == "Input":
input_blocks += 1
elif block.block_type.value == "Output":
output_blocks += 1
else:
action_blocks += 1
credentials = list(block.input_schema.get_credentials_fields().values())
if len(credentials) > 0:
integrations += 1
marketplace_agents = await prisma.models.StoreAgent.prisma().count()
_static_counts_cache = {
"all_blocks": all_blocks,
"input_blocks": input_blocks,
"action_blocks": action_blocks,
"output_blocks": output_blocks,
"integrations": integrations,
"marketplace_agents": marketplace_agents,
}
return _static_counts_cache
def _matches_llm_model(schema_cls: type[BlockSchema], query: str) -> bool:
for field in schema_cls.model_fields.values():
if field.annotation == LlmModel:
# Check if query matches any value in llm_models
if any(query in name for name in llm_models):
return True
return False
@functools.cache
def _get_all_providers() -> dict[ProviderName, Provider]:
providers: dict[ProviderName, Provider] = {}
for block_type in load_all_blocks().values():
block: Block[BlockSchema, BlockSchema] = block_type()
if block.disabled:
continue
credentials_info = block.input_schema.get_credentials_fields_info().values()
for info in credentials_info:
for provider in info.provider: # provider is a ProviderName enum member
if provider in providers:
providers[provider].integration_count += 1
else:
providers[provider] = Provider(
name=provider, description="", integration_count=1
)
return providers
async def get_suggested_blocks(count: int = 5) -> list[BlockData]:
global _suggested_blocks
if _suggested_blocks is not None and len(_suggested_blocks) >= count:
return _suggested_blocks[:count]
_suggested_blocks = []
# Sum the number of executions for each block type
# Prisma cannot group by nested relations, so we do a raw query
results = await prisma.get_client().query_raw(
"""
SELECT
agent_node."agentBlockId" AS block_id,
COUNT(execution.id) AS execution_count
FROM "AgentNodeExecution" execution
JOIN "AgentNode" agent_node ON execution."agentNodeId" = agent_node.id
GROUP BY agent_node."agentBlockId"
ORDER BY execution_count DESC;
"""
)
# Get the top blocks based on execution count
# But ignore Input and Output blocks
blocks: list[tuple[BlockData, int]] = []
for block_type in load_all_blocks().values():
block: Block[BlockSchema, BlockSchema] = block_type()
if block.disabled or block.block_type in (
backend.data.block.BlockType.INPUT,
backend.data.block.BlockType.OUTPUT,
backend.data.block.BlockType.AGENT,
):
continue
# Find the execution count for this block
execution_count = next(
(row["execution_count"] for row in results if row["block_id"] == block.id),
0,
)
blocks.append((block.to_dict(), execution_count))
# Sort blocks by execution count
blocks.sort(key=lambda x: x[1], reverse=True)
_suggested_blocks = [block[0] for block in blocks]
# Return the top blocks
return _suggested_blocks[:count]

View File

@@ -0,0 +1,87 @@
from typing import Any, Literal
from pydantic import BaseModel
import backend.server.model as server_model
import backend.server.v2.library.model as library_model
import backend.server.v2.store.model as store_model
from backend.integrations.providers import ProviderName
FilterType = Literal[
"blocks",
"integrations",
"marketplace_agents",
"my_agents",
]
BlockType = Literal["all", "input", "action", "output"]
BlockData = dict[str, Any]
# Suggestions
class SuggestionsResponse(BaseModel):
otto_suggestions: list[str]
recent_searches: list[str]
providers: list[ProviderName]
top_blocks: list[BlockData]
# All blocks
class BlockCategoryResponse(BaseModel):
name: str
total_blocks: int
blocks: list[BlockData]
model_config = {"use_enum_values": False} # <== use enum names like "AI"
# Input/Action/Output and see all for block categories
class BlockResponse(BaseModel):
blocks: list[BlockData]
pagination: server_model.Pagination
# Providers
class Provider(BaseModel):
name: ProviderName
description: str
integration_count: int
class ProviderResponse(BaseModel):
providers: list[Provider]
pagination: server_model.Pagination
# Search
class SearchRequest(BaseModel):
search_query: str | None = None
filter: list[FilterType] | None = None
by_creator: list[str] | None = None
search_id: str | None = None
page: int | None = None
page_size: int | None = None
class SearchBlocksResponse(BaseModel):
blocks: BlockResponse
total_block_count: int
total_integration_count: int
class SearchResponse(BaseModel):
items: list[BlockData | library_model.LibraryAgent | store_model.StoreAgent]
total_items: dict[FilterType, int]
page: int
more_pages: bool
class CountResponse(BaseModel):
all_blocks: int
input_blocks: int
action_blocks: int
output_blocks: int
integrations: int
marketplace_agents: int
my_agents: int

View File

@@ -0,0 +1,227 @@
import logging
from typing import Annotated, Sequence
import fastapi
from autogpt_libs.auth.depends import auth_middleware, get_user_id
import backend.server.model as server_model
import backend.server.v2.builder.db as builder_db
import backend.server.v2.builder.model as builder_model
import backend.server.v2.library.db as library_db
import backend.server.v2.library.model as library_model
import backend.server.v2.store.db as store_db
import backend.server.v2.store.model as store_model
from backend.integrations.providers import ProviderName
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
# Taken from backend/server/v2/store/db.py
def sanitize_query(query: str | None) -> str | None:
if query is None:
return query
query = query.strip()[:100]
return (
query.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("'", "\\'")
.replace('"', '\\"')
.replace(";", "\\;")
.replace("--", "\\--")
.replace("/*", "\\/*")
.replace("*/", "\\*/")
)
@router.get(
"/suggestions",
dependencies=[fastapi.Depends(auth_middleware)],
)
async def get_suggestions(
user_id: Annotated[str, fastapi.Depends(get_user_id)],
) -> builder_model.SuggestionsResponse:
"""
Get all suggestions for the Blocks Menu.
"""
return builder_model.SuggestionsResponse(
otto_suggestions=[
"What blocks do I need to get started?",
"Help me create a list",
"Help me feed my data to Google Maps",
],
recent_searches=[
"image generation",
"deepfake",
"competitor analysis",
],
providers=[
ProviderName.TWITTER,
ProviderName.GITHUB,
ProviderName.NOTION,
ProviderName.GOOGLE,
ProviderName.DISCORD,
ProviderName.GOOGLE_MAPS,
],
top_blocks=await builder_db.get_suggested_blocks(),
)
@router.get(
"/categories",
dependencies=[fastapi.Depends(auth_middleware)],
)
async def get_block_categories(
category_blocks: Annotated[int, fastapi.Query()] = 3,
) -> Sequence[builder_model.BlockCategoryResponse]:
"""
Get all block categories with a specified number of blocks per category.
"""
return builder_db.get_block_categories(category_blocks)
@router.get(
"/blocks",
dependencies=[fastapi.Depends(auth_middleware)],
)
async def get_blocks(
category: Annotated[str | None, fastapi.Query()] = None,
type: Annotated[builder_model.BlockType | None, fastapi.Query()] = None,
provider: Annotated[ProviderName | None, fastapi.Query()] = None,
page: Annotated[int, fastapi.Query()] = 1,
page_size: Annotated[int, fastapi.Query()] = 50,
) -> builder_model.BlockResponse:
"""
Get blocks based on either category, type, or provider.
"""
return builder_db.get_blocks(
category=category,
type=type,
provider=provider,
page=page,
page_size=page_size,
)
@router.get(
"/providers",
dependencies=[fastapi.Depends(auth_middleware)],
)
async def get_providers(
page: Annotated[int, fastapi.Query()] = 1,
page_size: Annotated[int, fastapi.Query()] = 50,
) -> builder_model.ProviderResponse:
"""
Get all integration providers with their block counts.
"""
return builder_db.get_providers(
page=page,
page_size=page_size,
)
@router.post(
"/search",
tags=["store", "private"],
dependencies=[fastapi.Depends(auth_middleware)],
)
async def search(
options: builder_model.SearchRequest,
user_id: Annotated[str, fastapi.Depends(get_user_id)],
) -> builder_model.SearchResponse:
"""
Search for blocks (including integrations), marketplace agents, and user library agents.
"""
# If no filters are provided, then we will return all types
if not options.filter:
options.filter = [
"blocks",
"integrations",
"marketplace_agents",
"my_agents",
]
options.search_query = sanitize_query(options.search_query)
options.page = options.page or 1
options.page_size = options.page_size or 50
# Blocks&Integrations
blocks = builder_model.SearchBlocksResponse(
blocks=builder_model.BlockResponse(
blocks=[],
pagination=server_model.Pagination.empty(),
),
total_block_count=0,
total_integration_count=0,
)
if "blocks" in options.filter or "integrations" in options.filter:
blocks = builder_db.search_blocks(
include_blocks="blocks" in options.filter,
include_integrations="integrations" in options.filter,
query=options.search_query or "",
page=options.page,
page_size=options.page_size,
)
# Library Agents
my_agents = library_model.LibraryAgentResponse(
agents=[],
pagination=server_model.Pagination.empty(),
)
if "my_agents" in options.filter:
my_agents = await library_db.list_library_agents(
user_id=user_id,
search_term=options.search_query,
page=options.page,
page_size=options.page_size,
)
# Marketplace Agents
marketplace_agents = store_model.StoreAgentsResponse(
agents=[],
pagination=server_model.Pagination.empty(),
)
if "marketplace_agents" in options.filter:
marketplace_agents = await store_db.get_store_agents(
creators=options.by_creator,
search_query=options.search_query,
page=options.page,
page_size=options.page_size,
)
more_pages = False
if (
blocks.blocks.pagination.current_page < blocks.blocks.pagination.total_pages
or my_agents.pagination.current_page < my_agents.pagination.total_pages
or marketplace_agents.pagination.current_page
< marketplace_agents.pagination.total_pages
):
more_pages = True
return builder_model.SearchResponse(
items=blocks.blocks.blocks + my_agents.agents + marketplace_agents.agents,
total_items={
"blocks": blocks.total_block_count,
"integrations": blocks.total_integration_count,
"marketplace_agents": marketplace_agents.pagination.total_items,
"my_agents": my_agents.pagination.total_items,
},
page=options.page,
more_pages=more_pages,
)
@router.get(
"/counts",
dependencies=[fastapi.Depends(auth_middleware)],
)
async def get_counts(
user_id: Annotated[str, fastapi.Depends(get_user_id)],
) -> builder_model.CountResponse:
"""
Get item counts for the menu categories in the Blocks Menu.
"""
return await builder_db.get_counts(user_id)

View File

@@ -444,7 +444,7 @@ async def add_store_agent_to_library(
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
},
include={"AgentGraph": True},
include=library_agent_include(user_id),
)
)
if existing_library_agent:

View File

@@ -42,6 +42,7 @@ class LibraryAgent(pydantic.BaseModel):
# Made input_schema and output_schema match GraphMeta's type
input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
output_schema: dict[str, Any]
# Indicates whether there's a new output (based on recent runs)
new_output: bool
@@ -106,6 +107,7 @@ class LibraryAgent(pydantic.BaseModel):
name=graph.name,
description=graph.description,
input_schema=graph.input_schema,
output_schema=graph.output_schema,
new_output=new_output,
can_access_graph=can_access_graph,
is_latest_version=is_latest_version,

View File

@@ -43,6 +43,7 @@ async def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
creator_name="Test Creator",
creator_image_url="",
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
status=library_model.LibraryAgentStatus.COMPLETED,
new_output=False,
can_access_graph=True,
@@ -59,6 +60,7 @@ async def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
creator_name="Test Creator",
creator_image_url="",
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
status=library_model.LibraryAgentStatus.COMPLETED,
new_output=False,
can_access_graph=False,

View File

@@ -37,7 +37,7 @@ def sanitize_query(query: str | None) -> str | None:
async def get_store_agents(
featured: bool = False,
creator: str | None = None,
creators: list[str] | None = None,
sorted_by: str | None = None,
search_query: str | None = None,
category: str | None = None,
@@ -48,15 +48,15 @@ async def get_store_agents(
Get PUBLIC store agents from the StoreAgent view
"""
logger.debug(
f"Getting store agents. featured={featured}, creator={creator}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}"
f"Getting store agents. featured={featured}, creator={creators}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}"
)
sanitized_query = sanitize_query(search_query)
where_clause = {}
if featured:
where_clause["featured"] = featured
if creator:
where_clause["creator_username"] = creator
if creators:
where_clause["creator_username"] = {"in": creators}
if category:
where_clause["categories"] = {"has": category}

View File

@@ -152,7 +152,7 @@ async def get_agents(
try:
agents = await backend.server.v2.store.db.get_store_agents(
featured=featured,
creator=creator,
creators=[creator] if creator else None,
sorted_by=sorted_by,
search_query=search_query,
category=category,

View File

@@ -81,9 +81,12 @@
"react-markdown": "^9.0.3",
"react-modal": "^3.16.3",
"react-shepherd": "^6.1.8",
"react-timeago": "^8.2.0",
"recharts": "^2.15.3",
"shepherd.js": "^14.5.0",
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar": "^4.0.2",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"zod": "^3.25.51"

View File

@@ -179,6 +179,9 @@ importers:
react-shepherd:
specifier: ^6.1.8
version: 6.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
react-timeago:
specifier: ^8.2.0
version: 8.2.0(react@18.3.1)
recharts:
specifier: ^2.15.3
version: 2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -188,6 +191,12 @@ importers:
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
tailwind-scrollbar:
specifier: ^4.0.2
version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17)
tailwind-scrollbar-hide:
specifier: ^2.0.0
version: 2.0.0(tailwindcss@3.4.17)
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17)
@@ -3142,6 +3151,9 @@ packages:
'@types/phoenix@1.6.6':
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
@@ -6442,6 +6454,11 @@ packages:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
react: '>=16.0.0'
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -6627,6 +6644,11 @@ packages:
'@types/react':
optional: true
react-timeago@8.2.0:
resolution: {integrity: sha512-RWDlG3Jj+iwv+yNEDweA/Qk1mxE8i/Oc4oW8Irp29ZfBp+eNpqqYPMLPYQJyfRMJcGB8CmWkEGMYhB4fW8eZlQ==}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@@ -7178,6 +7200,17 @@ packages:
tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
tailwind-scrollbar-hide@2.0.0:
resolution: {integrity: sha512-lqiIutHliEiODwBRHy4G2+Tcayo2U7+3+4frBmoMETD72qtah+XhOk5XcPzC1nJvXhXUdfl2ajlMhUc2qC6CIg==}
peerDependencies:
tailwindcss: '>=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20'
tailwind-scrollbar@4.0.2:
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 4.x
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
@@ -10972,6 +11005,8 @@ snapshots:
'@types/phoenix@1.6.6': {}
'@types/prismjs@1.26.5': {}
'@types/prop-types@15.7.14': {}
'@types/react-dom@18.3.7(@types/react@18.3.23)':
@@ -14940,6 +14975,12 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.3.1
prism-react-renderer@2.4.1(react@18.3.1):
dependencies:
'@types/prismjs': 1.26.5
clsx: 2.1.1
react: 18.3.1
process-nextick-args@2.0.1: {}
process-on-spawn@1.1.0:
@@ -15140,6 +15181,10 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.23
react-timeago@8.2.0(react@18.3.1):
dependencies:
react: 18.3.1
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.4
@@ -15854,6 +15899,17 @@ snapshots:
tailwind-merge@2.6.0: {}
tailwind-scrollbar-hide@2.0.0(tailwindcss@3.4.17):
dependencies:
tailwindcss: 3.4.17
tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17):
dependencies:
prism-react-renderer: 2.4.1(react@18.3.1)
tailwindcss: 3.4.17
transitivePeerDependencies:
- react
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
dependencies:
tailwindcss: 3.4.17

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -150,3 +150,11 @@ input[type="number"]::-webkit-inner-spin-button {
input[type="number"] {
-moz-appearance: textfield;
}
.scroll-container {
max-height: 200px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none; /* IE and Edge: hide scrollbar */
transition: scrollbar-width 0.3s ease;
}

View File

@@ -28,6 +28,7 @@ import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import {
Block,
BlockUIType,
formatEdgeID,
GraphExecutionID,
@@ -53,6 +54,7 @@ import OttoChatWidget from "@/components/OttoChatWidget";
import { useToast } from "@/components/ui/use-toast";
import { useCopyPaste } from "../hooks/useCopyPaste";
import { CronScheduler } from "./cronScheduler";
import { BlockMenu } from "./builder/block-menu/BlockMenu";
// This is for the history, this is the minimum distance a block must move before it is logged
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
@@ -103,7 +105,6 @@ const FlowEditor: React.FC<{
setAgentDescription,
savedAgent,
availableNodes,
availableFlows,
getOutputType,
requestSave,
requestSaveAndRun,
@@ -138,6 +139,10 @@ const FlowEditor: React.FC<{
// State to control if save popover should be pinned open
const [pinSavePopover, setPinSavePopover] = useState(false);
const [blockMenuSelected, setBlockMenuSelected] = useState<
"save" | "block" | ""
>("");
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
const [openCron, setOpenCron] = useState(false);
@@ -471,13 +476,7 @@ const FlowEditor: React.FC<{
}, [nodes, setViewport, x, y]);
const addNode = useCallback(
(blockId: string, nodeType: string, hardcodedValues: any = {}) => {
const nodeSchema = availableNodes.find((node) => node.id === blockId);
if (!nodeSchema) {
console.error(`Schema not found for block ID: ${blockId}`);
return;
}
(block: Block) => {
/*
Calculate a position to the right of the newly added block, allowing for some margin.
If adding to the right side causes the new block to collide with an existing block, attempt to place it at the bottom or left.
@@ -494,7 +493,7 @@ const FlowEditor: React.FC<{
? // we will get all the dimension of nodes, then store
findNewlyAddedBlockCoordinates(
nodeDimensions,
nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500,
block.uiType == BlockUIType.NOTE ? 300 : 500,
60,
1.0,
)
@@ -509,19 +508,19 @@ const FlowEditor: React.FC<{
type: "custom",
position: viewportCoordinates, // Set the position to the calculated viewport center
data: {
blockType: nodeType,
blockCosts: nodeSchema.costs,
title: `${nodeType} ${nodeId}`,
description: nodeSchema.description,
categories: nodeSchema.categories,
inputSchema: nodeSchema.inputSchema,
outputSchema: nodeSchema.outputSchema,
hardcodedValues: hardcodedValues,
blockType: block.name,
blockCosts: block.costs,
title: `${block.name} ${nodeId}`,
description: block.description,
categories: block.categories,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: block.hardcodedValues || {},
connections: [],
isOutputOpen: false,
block_id: blockId,
isOutputStatic: nodeSchema.staticOutput,
uiType: nodeSchema.uiType,
block_id: block.id,
isOutputStatic: block.staticOutput,
uiType: block.uiType,
},
};
@@ -550,7 +549,6 @@ const FlowEditor: React.FC<{
[
nodeId,
setViewport,
availableNodes,
addNodes,
nodeDimensions,
deleteElements,
@@ -632,12 +630,12 @@ const FlowEditor: React.FC<{
const editorControls: Control[] = [
{
label: "Undo",
icon: <IconUndo2 />,
icon: <IconUndo2 className="h-5 w-5" strokeWidth={2} />,
onClick: handleUndo,
},
{
label: "Redo",
icon: <IconRedo2 />,
icon: <IconRedo2 className="h-5 w-5" strokeWidth={2} />,
onClick: handleRedo,
},
];
@@ -685,15 +683,13 @@ const FlowEditor: React.FC<{
<Controls />
<Background className="dark:bg-slate-800" />
<ControlPanel
className="absolute z-20"
controls={editorControls}
topChildren={
<BlocksControl
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
blocks={availableNodes}
addBlock={addNode}
flows={availableFlows}
nodes={nodes}
<BlockMenu
pinBlocksPopover={pinBlocksPopover}
addNode={addNode}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>
}
botChildren={
@@ -706,6 +702,8 @@ const FlowEditor: React.FC<{
agentName={agentName}
onNameChange={setAgentName}
pinSavePopover={pinSavePopover}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>
}
></ControlPanel>

View File

@@ -0,0 +1,74 @@
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 "./IntegrationBlock";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
description?: string;
highlightedText?: string;
}
interface BlockComponent extends React.FC<Props> {
Skeleton: React.FC<{ className?: string }>;
}
const Block: BlockComponent = ({
title,
description,
highlightedText,
className,
...rest
}) => {
return (
<Button
className={cn(
"group flex h-16 w-full min-w-[7.5rem] items-center justify-start space-x-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
)}
{...rest}
>
<div className="flex flex-1 flex-col items-start gap-0.5">
<span
className={cn(
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
)}
>
{title && highlightText(beautifyString(title), highlightedText)}
</span>
<span
className={cn(
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
{description && highlightText(description, highlightedText)}
</span>
</div>
<div
className={cn(
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
)}
>
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
</div>
</Button>
);
};
const BlockSkeleton = () => {
return (
<Skeleton className="flex h-16 w-full min-w-[7.5rem] animate-pulse items-center justify-start space-x-3 rounded-[0.75rem] bg-zinc-100 px-[0.875rem] py-[0.625rem]">
<div className="flex flex-1 flex-col items-start gap-0.5">
<Skeleton className="h-[1.375rem] w-24 rounded bg-zinc-200" />
<Skeleton className="h-5 w-32 rounded bg-zinc-200" />
</div>
<Skeleton className="h-7 w-7 rounded-[0.5rem] bg-zinc-200" />
</Skeleton>
);
};
Block.Skeleton = BlockSkeleton;
export default Block;

View File

@@ -0,0 +1,65 @@
import React, { useCallback, useState } from "react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
import { ToyBrick } from "lucide-react";
import BlockMenuContent from "./BlockMenuContent";
import { BlockMenuStateProvider } from "./block-menu-provider";
import { Block } from "@/lib/autogpt-server-api";
interface BlockMenuProps {
addNode: (block: Block) => void;
pinBlocksPopover: boolean;
blockMenuSelected: "save" | "block" | "";
setBlockMenuSelected: React.Dispatch<
React.SetStateAction<"" | "save" | "block">
>;
}
export const BlockMenu: React.FC<BlockMenuProps> = ({
addNode,
pinBlocksPopover,
blockMenuSelected,
setBlockMenuSelected,
}) => {
const [open, setOpen] = useState(false);
const onOpen = useCallback(
(newOpen: boolean) => {
if (!pinBlocksPopover) {
setOpen(newOpen);
setBlockMenuSelected(newOpen ? "block" : "");
}
},
[pinBlocksPopover, setOpen, setBlockMenuSelected],
);
return (
<Popover open={pinBlocksPopover ? true : open} onOpenChange={onOpen}>
<PopoverTrigger className="hover:cursor-pointer">
<ControlPanelButton
data-id="blocks-control-popover-trigger"
data-testid="blocks-control-blocks-button"
selected={blockMenuSelected === "block"}
className="rounded-none"
>
<ToyBrick className="h-5 w-6" strokeWidth={2} />
</ControlPanelButton>
</PopoverTrigger>
<PopoverContent
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)]"
data-id="blocks-control-popover-content"
>
<BlockMenuStateProvider addNode={addNode}>
<BlockMenuContent />
</BlockMenuStateProvider>
</PopoverContent>
</Popover>
);
};

View File

@@ -0,0 +1,20 @@
"use client";
import React, { useState } from "react";
import BlockMenuSearchBar from "./BlockMenuSearchBar";
import BlockMenuSearch from "./search-and-filter//BlockMenuSearch";
import BlockMenuDefault from "./default/BlockMenuDefault";
import { Separator } from "@/components/ui/separator";
import { useBlockMenuContext } from "./block-menu-provider";
const BlockMenuContent: React.FC = () => {
const { searchQuery } = useBlockMenuContext();
return (
<div className="flex h-full w-full flex-col">
<BlockMenuSearchBar />
<Separator className="h-[1px] w-full text-zinc-300" />
{searchQuery ? <BlockMenuSearch /> : <BlockMenuDefault />}
</div>
);
};
export default BlockMenuContent;

View File

@@ -0,0 +1,93 @@
import { cn } from "@/lib/utils";
import { Search, X } from "lucide-react";
import React, { useRef, useState, useEffect, useMemo } from "react";
import { useBlockMenuContext } from "./block-menu-provider";
import { Button } from "@/components/ui/button";
import debounce from "lodash/debounce";
import { Input } from "@/components/ui/input";
interface BlockMenuSearchBarProps {
className?: string;
}
const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
className = "",
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const [localQuery, setLocalQuery] = useState("");
const { setSearchQuery, searchId, setSearchId, setFilters } =
useBlockMenuContext();
const debouncedSetSearchQuery = useMemo(
() =>
debounce((value: string) => {
setSearchQuery(value);
if (value.length === 0) {
setSearchId(undefined);
} else if (!searchId) {
setSearchId(crypto.randomUUID());
}
}, 500),
[setSearchQuery, setSearchId, searchId],
);
useEffect(() => {
return () => {
debouncedSetSearchQuery.cancel();
};
}, [debouncedSetSearchQuery]);
const handleClear = () => {
setLocalQuery("");
setSearchQuery("");
setSearchId(undefined);
setFilters({
categories: {
blocks: false,
integrations: false,
marketplace_agents: false,
my_agents: false,
providers: false,
},
createdBy: [],
});
debouncedSetSearchQuery.cancel();
};
return (
<div
className={cn(
"flex min-h-[3.5625rem] items-center gap-2.5 px-4",
className,
)}
>
<Search className="h-6 w-6 text-zinc-700" strokeWidth={2} />
<Input
ref={inputRef}
type="text"
value={localQuery}
onChange={(e) => {
setLocalQuery(e.target.value);
debouncedSetSearchQuery(e.target.value);
}}
placeholder={"Blocks, Agents, Integrations or Keywords..."}
className={cn(
"m-0 border-none p-0 font-sans text-base font-normal text-zinc-800 shadow-none outline-none",
"placeholder:text-zinc-400 focus:shadow-none focus:outline-none focus:ring-0",
)}
/>
{localQuery.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleClear}
className="p-0 hover:bg-transparent"
>
<X className="h-6 w-6 text-zinc-700" strokeWidth={2} />
</Button>
)}
</div>
);
};
export default BlockMenuSearchBar;

View File

@@ -0,0 +1,35 @@
// BLOCK MENU TODO: We need a disable state in this, currently it's not in design.
import { cn } from "@/lib/utils";
import React from "react";
interface Props extends React.HTMLAttributes<HTMLDivElement> {
selected?: boolean;
children?: React.ReactNode; // For icon purpose
disabled?: boolean;
}
const ControlPanelButton: React.FC<Props> = ({
selected = false,
children,
disabled,
className,
...rest
}) => {
return (
// Using div instead of button, because it's only for design purposes. We are using this to give design to PopoverTrigger.
<div
className={cn(
"flex h-[4.25rem] w-[4.25rem] items-center justify-center whitespace-normal bg-white p-[1.38rem] text-zinc-800 shadow-none hover:cursor-pointer hover:bg-zinc-100 hover:text-zinc-950 focus:ring-0",
selected &&
"bg-violet-50 text-violet-700 hover:cursor-default hover:bg-violet-50 hover:text-violet-700 active:bg-violet-50 active:text-violet-700",
disabled && className,
)}
{...rest}
>
{children}
</div>
);
};
export default ControlPanelButton;

View File

@@ -0,0 +1,58 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AlertCircle, RefreshCw } from "lucide-react";
import React from "react";
interface ErrorStateProps {
title?: string;
message?: string;
error?: string | Error | null;
onRetry?: () => void;
retryLabel?: string;
className?: string;
showIcon?: boolean;
}
const ErrorState: React.FC<ErrorStateProps> = ({
title = "Something went wrong",
message,
error,
onRetry,
retryLabel = "Retry",
className,
showIcon = true,
}) => {
const errorMessage = error
? error instanceof Error
? error.message
: String(error)
: message || "An unexpected error occurred. Please try again.";
const classes =
"flex h-full w-full flex-col items-center justify-center text-center space-y-4";
return (
<div className={cn(classes, className)}>
{showIcon && <AlertCircle className="h-12 w-12" strokeWidth={1.5} />}
<div className="space-y-2">
<p className="text-sm font-medium text-zinc-800">{title}</p>
<p className="text-sm text-zinc-600">{errorMessage}</p>
</div>
{onRetry && (
<Button
variant="default"
size="sm"
onClick={onRetry}
className="mt-2 h-7 bg-zinc-800 text-xs"
>
<RefreshCw className="mr-1 h-3 w-3" />
{retryLabel}
</Button>
)}
</div>
);
};
export default ErrorState;

View File

@@ -0,0 +1,56 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
import React, { ButtonHTMLAttributes, useState } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
selected?: boolean;
number?: number;
name?: string;
}
const FilterChip: React.FC<Props> = ({
selected = false,
number,
name,
className,
...rest
}) => {
const [isHovering, setIsHovering] = useState(false);
return (
<Button
className={cn(
"group w-fit space-x-1 rounded-[1.5rem] border border-zinc-300 bg-transparent px-[0.625rem] py-[0.375rem] shadow-none transition-transform duration-300 ease-in-out",
"hover:border-violet-500 hover:bg-transparent focus:ring-0 disabled:pointer-events-none",
selected && "border-0 bg-violet-700 hover:border",
)}
{...rest}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<span
className={cn(
"font-sans text-sm font-medium leading-[1.375rem] text-zinc-600 group-hover:text-zinc-600 group-disabled:text-zinc-400",
selected && "text-zinc-50",
)}
>
{name}
</span>
{selected &&
(isHovering && number !== undefined ? (
<span className="flex h-[1.375rem] items-center rounded-[1.25rem] bg-violet-700 p-[0.375rem] text-zinc-50 transition-all duration-300 ease-in-out animate-in fade-in zoom-in">
{number > 100 ? "100+" : number}
</span>
) : (
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-zinc-50 transition-all duration-300 ease-in-out">
<X
className="h-3 w-3 rounded-full text-violet-700"
strokeWidth={2}
/>
</span>
))}
</Button>
);
};
export default FilterChip;

View File

@@ -0,0 +1,88 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { beautifyString, cn } from "@/lib/utils";
import Image from "next/image";
import React, { ButtonHTMLAttributes } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
description?: string;
icon_url?: string;
number_of_blocks?: number;
}
interface IntegrationComponent extends React.FC<Props> {
Skeleton: React.FC<{ className?: string }>;
}
const Integration: IntegrationComponent = ({
title,
icon_url,
description,
className,
number_of_blocks,
...rest
}) => {
return (
<Button
className={cn(
"group flex h-16 w-full min-w-[7.5rem] items-center justify-start space-x-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-50 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
className,
)}
{...rest}
>
<div className="relative h-[2.625rem] w-[2.625rem] overflow-hidden rounded-[0.5rem] bg-white">
{icon_url && (
<Image
src={icon_url}
alt="integration-icon"
fill
sizes="2.25rem"
className="w-full rounded-[0.5rem] object-contain group-disabled:opacity-50"
/>
)}
</div>
<div className="w-full">
<div className="flex items-center justify-between gap-2">
<p className="line-clamp-1 flex-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-700 group-disabled:text-zinc-400">
{title && beautifyString(title)}
</p>
<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">
{number_of_blocks}
</span>
</div>
<span className="line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400">
{description}
</span>
</div>
</Button>
);
};
const IntegrationSkeleton: React.FC<{ className?: string }> = ({
className,
}) => {
return (
<Skeleton
className={cn(
"flex h-16 w-full min-w-[7.5rem] animate-pulse items-center justify-start space-x-3 rounded-[0.75rem] bg-zinc-100 px-[0.875rem] py-[0.625rem]",
className,
)}
>
<Skeleton className="h-[2.625rem] w-[2.625rem] rounded-[0.5rem] bg-zinc-200" />
<div className="flex flex-1 flex-col items-start gap-0.5">
<div className="flex w-full items-center justify-between">
<Skeleton className="h-[1.375rem] w-24 rounded bg-zinc-200" />
<Skeleton className="h-[1.375rem] w-[1.6875rem] rounded-[1.25rem] bg-zinc-200" />
</div>
<Skeleton className="h-5 w-[80%] rounded bg-zinc-200" />
</div>
</Skeleton>
);
};
Integration.Skeleton = IntegrationSkeleton;
export default Integration;

View File

@@ -0,0 +1,110 @@
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";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
description?: string;
icon_url?: string;
highlightedText?: string;
}
interface IntegrationBlockComponent extends React.FC<Props> {
Skeleton: React.FC<{ className?: string }>;
}
export const highlightText = (
text: string | undefined,
highlight: string | undefined,
) => {
if (!text || !highlight) return text;
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
return parts.map((part, i) =>
part.toLowerCase() === highlight?.toLowerCase() ? (
<mark key={i} className="bg-transparent font-bold">
{part}
</mark>
) : (
part
),
);
};
const IntegrationBlock: IntegrationBlockComponent = ({
title,
icon_url,
description,
className,
highlightedText,
...rest
}) => {
return (
<Button
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:pointer-events-none",
)}
{...rest}
>
<div className="relative h-[2.625rem] w-[2.625rem] rounded-[0.5rem] bg-white">
{icon_url && (
<Image
src={icon_url}
alt="integration-icon"
fill
sizes="2.25rem"
className="w-full object-contain group-disabled:opacity-50"
/>
)}
</div>
<div className="flex flex-1 flex-col items-start gap-0.5">
<span
className={cn(
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
)}
>
{title && highlightText(beautifyString(title), highlightedText)}
</span>
<span
className={cn(
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
{description && highlightText(description, highlightedText)}
</span>
</div>
<div
className={cn(
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
)}
>
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
</div>
</Button>
);
};
const IntegrationBlockSkeleton = ({ className }: { className?: string }) => {
return (
<Skeleton
className={cn(
"flex h-16 w-full min-w-[7.5rem] animate-pulse items-center justify-start gap-3 rounded-[0.75rem] bg-zinc-100 px-[0.875rem] py-[0.625rem]",
className,
)}
>
<Skeleton className="h-[2.625rem] w-[2.625rem] rounded-[0.5rem] bg-zinc-200" />
<div className="flex flex-1 flex-col items-start gap-0.5">
<Skeleton className="h-[1.375rem] w-24 rounded bg-zinc-200" />
<Skeleton className="h-5 w-32 rounded bg-zinc-200" />
</div>
<Skeleton className="h-7 w-7 rounded-[0.5rem] bg-zinc-200" />
</Skeleton>
);
};
IntegrationBlock.Skeleton = IntegrationBlockSkeleton;
export default IntegrationBlock;

View File

@@ -0,0 +1,60 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { beautifyString, cn } from "@/lib/utils";
import Image from "next/image";
import React, { ButtonHTMLAttributes } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
name?: string;
icon_url?: string;
}
interface IntegrationChipComponent extends React.FC<Props> {
Skeleton: React.FC;
}
const IntegrationChip: IntegrationChipComponent = ({
icon_url,
name,
className,
...rest
}) => {
return (
<Button
className={cn(
"flex h-[3.25rem] w-full min-w-[7.5rem] justify-start gap-2 whitespace-normal rounded-[0.5rem] bg-zinc-50 p-2 pr-3 shadow-none",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300",
className,
)}
{...rest}
>
<div className="relative h-9 w-9 rounded-[0.5rem] bg-transparent">
{icon_url && (
<Image
src={icon_url}
alt="integration-icon"
fill
sizes="2.25rem"
className="w-full object-contain"
/>
)}
</div>
<span className="truncate font-sans text-sm font-normal leading-[1.375rem] text-zinc-800">
{name && beautifyString(name)}
</span>
</Button>
);
};
const IntegrationChipSkeleton: React.FC = () => {
return (
<Skeleton className="flex h-[3.25rem] w-full min-w-[7.5rem] gap-2 rounded-[0.5rem] bg-zinc-100 p-2 pr-3">
<Skeleton className="h-9 w-12 rounded-[0.5rem] bg-zinc-200" />
<Skeleton className="h-5 w-24 self-center rounded-sm bg-zinc-200" />
</Skeleton>
);
};
IntegrationChip.Skeleton = IntegrationChipSkeleton;
export default IntegrationChip;

View File

@@ -0,0 +1,135 @@
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 { highlightText } from "./IntegrationBlock";
import Link from "next/link";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
creator_name?: string;
number_of_runs?: number;
image_url?: string;
highlightedText?: string;
slug: string;
loading: boolean;
}
interface MarketplaceAgentBlockComponent extends React.FC<Props> {
Skeleton: React.FC<{ className?: string }>;
}
const MarketplaceAgentBlock: MarketplaceAgentBlockComponent = ({
title,
image_url,
creator_name,
number_of_runs,
className,
loading,
highlightedText,
slug,
...rest
}) => {
return (
<Button
className={cn(
"group flex h-[4.375rem] w-full min-w-[7.5rem] items-center justify-start gap-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 p-[0.625rem] pr-[0.875rem] 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:pointer-events-none",
className,
)}
{...rest}
>
<div className="relative h-[3.125rem] w-[5.625rem] overflow-hidden rounded-[0.375rem] bg-white">
{image_url && (
<Image
src={image_url}
alt="integration-icon"
fill
sizes="5.625rem"
className="w-full object-contain group-disabled:opacity-50"
/>
)}
</div>
<div className="flex flex-1 flex-col items-start gap-0.5">
<span
className={cn(
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
)}
>
{highlightText(title, highlightedText)}
</span>
<div className="flex items-center space-x-2.5">
<span
className={cn(
"truncate font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
By {creator_name}
</span>
<span className="font-sans text-zinc-400"></span>
<span
className={cn(
"truncate font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
{number_of_runs} runs
</span>
<span className="font-sans text-zinc-400"></span>
<Link
href={`/marketplace/agent/${creator_name}/${slug}`}
className="flex gap-0.5 truncate"
onClick={(e) => e.stopPropagation()}
>
<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} />
</Link>
</div>
</div>
<div
className={cn(
"flex h-7 min-w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
)}
>
{!loading ? (
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
) : (
<Loader2 className="h-5 w-5 animate-spin" />
)}
</div>
</Button>
);
};
const MarketplaceAgentBlockSkeleton: React.FC<{ className?: string }> = ({
className,
}) => {
return (
<Skeleton
className={cn(
"flex h-[4.375rem] w-full min-w-[7.5rem] animate-pulse items-center justify-start gap-3 rounded-[0.75rem] bg-zinc-100 p-[0.625rem] pr-[0.875rem]",
className,
)}
>
<Skeleton className="h-[3.125rem] w-[5.625rem] rounded-[0.375rem] bg-zinc-200" />
<div className="flex flex-1 flex-col items-start gap-0.5">
<Skeleton className="h-[1.375rem] w-24 rounded bg-zinc-200" />
<div className="flex items-center gap-1">
<Skeleton className="h-5 w-16 rounded bg-zinc-200" />
<Skeleton className="h-5 w-16 rounded bg-zinc-200" />
</div>
</div>
<Skeleton className="h-7 w-7 rounded-[0.5rem] bg-zinc-200" />
</Skeleton>
);
};
MarketplaceAgentBlock.Skeleton = MarketplaceAgentBlockSkeleton;
export default MarketplaceAgentBlock;

View File

@@ -0,0 +1,42 @@
// BLOCK MENU TODO: We need to add a better hover state to it; currently it's not in the design either.
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import React, { ButtonHTMLAttributes } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
selected?: boolean;
number?: number;
name?: string;
}
const MenuItem: React.FC<Props> = ({
selected = false,
number,
name,
className,
...rest
}) => {
return (
<Button
className={cn(
"flex h-[2.375rem] w-[12.875rem] justify-between whitespace-normal rounded-[0.5rem] bg-transparent p-2 pl-3 shadow-none",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0",
selected && "bg-zinc-100",
className,
)}
{...rest}
>
<span className="truncate font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
{name}
</span>
{number && (
<span className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-600">
{number > 100 ? "100+" : number}
</span>
)}
</Button>
);
};
export default MenuItem;

View File

@@ -0,0 +1,49 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { ArrowUpRight } from "lucide-react";
import React, { ButtonHTMLAttributes } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
content?: string;
}
interface SearchHistoryChipComponent extends React.FC<Props> {
Skeleton: React.FC<{ className?: string }>;
}
const SearchHistoryChip: SearchHistoryChipComponent = ({
content,
className,
...rest
}) => {
return (
<Button
className={cn(
"my-[1px] h-[2.25rem] space-x-1 rounded-[1.5rem] bg-zinc-50 p-[0.375rem] pr-[0.625rem] shadow-none",
"hover:cursor-default hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300",
className,
)}
{...rest}
>
<ArrowUpRight className="h-6 w-6 text-zinc-500" strokeWidth={1.25} />
<span className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-800">
{content}
</span>
</Button>
);
};
const SearchHistoryChipSkeleton: React.FC<{ className?: string }> = ({
className,
}) => {
return (
<Skeleton
className={cn("h-[2.25rem] w-32 rounded-[1.5rem] bg-zinc-100", className)}
/>
);
};
SearchHistoryChip.Skeleton = SearchHistoryChipSkeleton;
export default SearchHistoryChip;

View File

@@ -0,0 +1,115 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { Plus } from "lucide-react";
import Image from "next/image";
import React, { ButtonHTMLAttributes } from "react";
import { highlightText } from "./IntegrationBlock";
import TimeAgo from "react-timeago";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
edited_time?: Date;
version?: number;
image_url?: string;
highlightedText?: string;
}
interface UGCAgentBlockComponent extends React.FC<Props> {
Skeleton: React.FC<{ className?: string }>;
}
const UGCAgentBlock: UGCAgentBlockComponent = ({
title,
image_url,
edited_time,
version,
className,
highlightedText,
...rest
}) => {
return (
<Button
className={cn(
"group flex h-[4.375rem] w-full min-w-[7.5rem] items-center justify-start gap-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 p-[0.625rem] pr-[0.875rem] 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:pointer-events-none",
className,
)}
{...rest}
>
<div className="relative h-[3.125rem] w-[5.625rem] overflow-hidden rounded-[0.375rem] bg-white">
{image_url && (
<Image
src={image_url}
alt="integration-icon"
fill
sizes="5.625rem"
className="w-full object-contain group-disabled:opacity-50"
/>
)}
</div>
<div className="flex flex-1 flex-col items-start gap-0.5">
<span
className={cn(
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
)}
>
{highlightText(title, highlightedText)}
</span>
<div className="flex items-center space-x-1.5">
<span
className={cn(
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
Edited {edited_time && <TimeAgo date={edited_time} />}
</span>
<span className="font-sans text-zinc-400"></span>
<span
className={cn(
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
Version {version}
</span>
</div>
</div>
<div
className={cn(
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
)}
>
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
</div>
</Button>
);
};
const UGCAgentBlockSkeleton: React.FC<{ className?: string }> = ({
className,
}) => {
return (
<Skeleton
className={cn(
"flex h-[4.375rem] w-full min-w-[7.5rem] animate-pulse items-center justify-start gap-3 rounded-[0.75rem] bg-zinc-100 p-[0.625rem] pr-[0.875rem]",
className,
)}
>
<Skeleton className="h-[3.125rem] w-[5.625rem] rounded-[0.375rem] bg-zinc-200" />
<div className="flex flex-1 flex-col items-start gap-0.5">
<Skeleton className="h-[1.375rem] w-24 rounded bg-zinc-200" />
<div className="flex items-center gap-1">
<Skeleton className="h-5 w-16 rounded bg-zinc-200" />
<Skeleton className="h-5 w-16 rounded bg-zinc-200" />
</div>
</div>
<Skeleton className="h-7 w-7 rounded-[0.5rem] bg-zinc-200" />
</Skeleton>
);
};
UGCAgentBlock.Skeleton = UGCAgentBlockSkeleton;
export default UGCAgentBlock;

View File

@@ -0,0 +1,182 @@
"use client";
import {
Block,
CredentialsProviderName,
LibraryAgent,
Provider,
StoreAgent,
} from "@/lib/autogpt-server-api";
import { createContext, ReactNode, useContext, useState } from "react";
import { convertLibraryAgentIntoBlock } from "@/lib/utils";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export type SearchItem = Block | Provider | LibraryAgent | StoreAgent;
export type DefaultStateType =
| "suggestion"
| "all_blocks"
| "input_blocks"
| "action_blocks"
| "output_blocks"
| "integrations"
| "marketplace_agents"
| "my_agents";
export type CategoryKey =
| "blocks"
| "integrations"
| "marketplace_agents"
| "my_agents";
export interface Filters {
categories: {
blocks: boolean;
integrations: boolean;
marketplace_agents: boolean;
my_agents: boolean;
providers: boolean;
};
createdBy: string[];
}
export type CategoryCounts = Record<CategoryKey, number>;
interface BlockMenuContextType {
defaultState: DefaultStateType;
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
integration: CredentialsProviderName | null;
setIntegration: React.Dispatch<
React.SetStateAction<CredentialsProviderName | null>
>;
searchQuery: string;
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
searchId: string | undefined;
setSearchId: React.Dispatch<React.SetStateAction<string | undefined>>;
filters: Filters;
setFilters: React.Dispatch<React.SetStateAction<Filters>>;
searchData: SearchItem[];
setSearchData: React.Dispatch<React.SetStateAction<SearchItem[]>>;
categoryCounts: CategoryCounts;
setCategoryCounts: React.Dispatch<React.SetStateAction<CategoryCounts>>;
addNode: (block: Block) => void;
handleAddStoreAgent: ({
creator_name,
slug,
}: {
creator_name: string;
slug: string;
}) => Promise<void>;
loadingSlug: string | null;
setLoadingSlug: React.Dispatch<React.SetStateAction<string | null>>;
}
export const BlockMenuContext = createContext<BlockMenuContextType>(
{} as BlockMenuContextType,
);
interface BlockMenuStateProviderProps {
children: ReactNode;
addNode: (block: Block) => void;
}
export function BlockMenuStateProvider({
children,
addNode,
}: BlockMenuStateProviderProps) {
const [defaultState, setDefaultState] =
useState<DefaultStateType>("suggestion");
const [integration, setIntegration] =
useState<CredentialsProviderName | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [filters, setFilters] = useState<Filters>({
categories: {
blocks: false,
integrations: false,
marketplace_agents: false,
my_agents: false,
providers: false,
},
createdBy: [],
});
const [searchData, setSearchData] = useState<SearchItem[]>([]);
const [searchId, setSearchId] = useState<string | undefined>(undefined);
const [categoryCounts, setCategoryCounts] = useState<CategoryCounts>({
blocks: 0,
integrations: 0,
marketplace_agents: 0,
my_agents: 0,
});
const [loadingSlug, setLoadingSlug] = useState<string | null>(null);
const api = useBackendAPI();
const handleAddStoreAgent = async ({
creator_name,
slug,
}: {
creator_name: string;
slug: string;
}) => {
try {
setLoadingSlug(slug);
const details = await api.getStoreAgent(creator_name, slug);
if (!details.active_version_id) {
console.error(
"Cannot add store agent to library: active version ID is missing or undefined",
);
return;
}
const libraryAgent = await api.addMarketplaceAgentToLibrary(
details.active_version_id,
);
const block = convertLibraryAgentIntoBlock(libraryAgent);
addNode(block);
} catch (error) {
console.error("Failed to add store agent:", error);
} finally {
setLoadingSlug(null);
}
};
return (
<BlockMenuContext.Provider
value={{
defaultState,
setDefaultState,
integration,
setIntegration,
searchQuery,
setSearchQuery,
searchId,
setSearchId,
filters,
setFilters,
searchData,
setSearchData,
categoryCounts,
setCategoryCounts,
addNode,
handleAddStoreAgent,
loadingSlug,
setLoadingSlug,
}}
>
{children}
</BlockMenuContext.Provider>
);
}
export function useBlockMenuContext(): BlockMenuContextType {
const context = useContext(BlockMenuContext);
if (!context) {
throw new Error("Error in context of Block");
}
return context;
}

View File

@@ -0,0 +1,8 @@
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const ActionBlocksContent: React.FC = () => {
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
};
export default ActionBlocksContent;

View File

@@ -0,0 +1,163 @@
import React, { useState, useEffect, Fragment, useCallback } from "react";
import Block from "../Block";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { BlockCategoryResponse } from "@/lib/autogpt-server-api";
import { useBlockMenuContext } from "../block-menu-provider";
import ErrorState from "../ErrorState";
import { beautifyString } from "@/lib/utils";
const AllBlocksContent: React.FC = () => {
const { addNode } = useBlockMenuContext();
const [categories, setCategories] = useState<BlockCategoryResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [loadingCategories, setLoadingCategories] = useState<Set<string>>(
new Set(),
);
const api = useBackendAPI();
const fetchBlocks = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await api.getBlockCategories();
setCategories(response);
} catch (err) {
console.error("Failed to fetch block categories:", err);
setError(
err instanceof Error ? err.message : "Failed to load block categories",
);
} finally {
setLoading(false);
}
}, [api]);
useEffect(() => {
fetchBlocks();
}, [api, fetchBlocks]);
const fetchMoreBlockOfACategory = async (category: string) => {
try {
setLoadingCategories((prev) => new Set(prev).add(category));
const response = await api.getBuilderBlocks({ category: category });
const updatedCategories = categories.map((cat) => {
if (cat.name === category) {
return {
...cat,
blocks: [...response.blocks],
};
}
return cat;
});
setCategories(updatedCategories);
} catch (error) {
console.error(`Failed to fetch blocks for category ${category}:`, error);
} finally {
setLoadingCategories((prev) => {
const newSet = new Set(prev);
newSet.delete(category);
return newSet;
});
}
};
if (loading) {
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 3 }).map((_, categoryIndex) => (
<Fragment key={categoryIndex}>
{categoryIndex > 0 && (
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
)}
{[0, 1, 2].map((blockIndex) => (
<Block.Skeleton key={`${categoryIndex}-${blockIndex}`} />
))}
</Fragment>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load blocks"
error={error}
onRetry={fetchBlocks}
/>
</div>
);
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full space-y-3 px-4 pb-4">
{categories.map((category, index) => (
<Fragment key={category.name}>
{index > 0 && (
<Separator className="h-[1px] w-full text-zinc-300" />
)}
{/* Category Section */}
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
{category.name && beautifyString(category.name)}
</p>
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
{category.total_blocks}
</span>
</div>
<div className="space-y-2">
{category.blocks.map((block, idx) => (
<Block
key={`${category.name}-${idx}`}
title={block.name}
description={block.name}
onClick={() => {
addNode(block);
}}
/>
))}
{loadingCategories.has(category.name) && (
<>
{[0, 1, 2, 3, 4].map((skeletonIndex) => (
<Block.Skeleton
key={`skeleton-${category.name}-${skeletonIndex}`}
/>
))}
</>
)}
{category.total_blocks > category.blocks.length && (
<Button
variant={"link"}
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
disabled={loadingCategories.has(category.name)}
onClick={() => {
fetchMoreBlockOfACategory(category.name);
}}
>
see all
</Button>
)}
</div>
</div>
</Fragment>
))}
</div>
</div>
);
};
export default AllBlocksContent;

View File

@@ -0,0 +1,16 @@
import React from "react";
import BlockMenuSidebar from "./BlockMenuSidebar";
import { Separator } from "@/components/ui/separator";
import BlockMenuDefaultContent from "./BlockMenuDefaultContent";
const BlockMenuDefault: React.FC = () => {
return (
<div className="flex flex-1 overflow-y-auto">
<BlockMenuSidebar />
<Separator className="h-full w-[1px] text-zinc-300" />
<BlockMenuDefaultContent />
</div>
);
};
export default BlockMenuDefault;

View File

@@ -0,0 +1,41 @@
import React from "react";
import SuggestionContent from "./SuggestionContent";
import AllBlocksContent from "./AllBlocksContent";
import IntegrationsContent from "./IntegrationsContent";
import MarketplaceAgentsContent from "./MarketplaceAgentsContent";
import MyAgentsContent from "./MyAgentsContent";
import ActionBlocksContent from "./ActionBlocksContent";
import InputBlocksContent from "./InputBlocksContent";
import OutputBlocksContent from "./OutputBlocksContent";
import { useBlockMenuContext } from "../block-menu-provider";
export interface ActionBlock {
id: number;
title: string;
description: string;
}
export interface BlockListType {
id: number;
title: string;
description: string;
}
const BlockMenuDefaultContent: React.FC = ({}) => {
const { defaultState } = useBlockMenuContext();
return (
<div className="h-full flex-1 overflow-hidden">
{defaultState == "suggestion" && <SuggestionContent />}
{defaultState == "all_blocks" && <AllBlocksContent />}
{defaultState == "input_blocks" && <InputBlocksContent />}
{defaultState == "action_blocks" && <ActionBlocksContent />}
{defaultState == "output_blocks" && <OutputBlocksContent />}
{defaultState == "integrations" && <IntegrationsContent />}
{defaultState == "marketplace_agents" && <MarketplaceAgentsContent />}
{defaultState == "my_agents" && <MyAgentsContent />}
</div>
);
};
export default BlockMenuDefaultContent;

View File

@@ -0,0 +1,119 @@
import React, { useEffect, useState } from "react";
import MenuItem from "../MenuItem";
import { DefaultStateType, useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { CountResponse } from "@/lib/autogpt-server-api";
const BlockMenuSidebar: React.FC = ({}) => {
const { defaultState, setDefaultState, setIntegration } =
useBlockMenuContext();
const [blockCounts, setBlockCounts] = useState<CountResponse | undefined>(
undefined,
);
const api = useBackendAPI();
useEffect(() => {
const fetchBlockCounts = async () => {
try {
const counts = await api.getBlockCounts();
setBlockCounts(counts);
} catch (error) {
console.error("Failed to fetch block counts:", error);
}
};
fetchBlockCounts();
}, [api]);
const topLevelMenuItems = [
{
name: "Suggestion",
type: "suggestion",
},
{
name: "All blocks",
type: "all_blocks",
number: blockCounts?.all_blocks,
},
];
const subMenuItems = [
{
name: "Input blocks",
type: "input_blocks",
number: blockCounts?.input_blocks,
},
{
name: "Action blocks",
type: "action_blocks",
number: blockCounts?.action_blocks,
},
{
name: "Output blocks",
type: "output_blocks",
number: blockCounts?.output_blocks,
},
];
const bottomMenuItems = [
{
name: "Integrations",
type: "integrations",
number: blockCounts?.integrations,
onClick: () => {
setIntegration(null);
setDefaultState("integrations");
},
},
{
name: "Marketplace Agents",
type: "marketplace_agents",
number: blockCounts?.marketplace_agents,
},
{
name: "My Agents",
type: "my_agents",
number: blockCounts?.my_agents,
},
];
return (
<div className="w-fit space-y-2 px-4 pt-4">
{topLevelMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
))}
<div className="ml-[0.5365rem] space-y-2 border-l border-black/10 pl-[0.75rem]">
{subMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
className="max-w-[11.5339rem]"
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
))}
</div>
{bottomMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
selected={defaultState === item.type}
onClick={
item.onClick ||
(() => setDefaultState(item.type as DefaultStateType))
}
/>
))}
</div>
);
};
export default BlockMenuSidebar;

View File

@@ -0,0 +1,33 @@
import React from "react";
import Block from "../Block";
import { Block as BlockType } from "@/lib/autogpt-server-api";
import { useBlockMenuContext } from "../block-menu-provider";
interface BlocksListProps {
blocks: BlockType[];
loading?: boolean;
}
const BlocksList: React.FC<BlocksListProps> = ({ blocks, loading = false }) => {
const { addNode } = useBlockMenuContext();
return (
<div className="w-full space-y-3 px-4 pb-4">
{loading
? Array.from({ length: 7 }).map((_, index) => (
<Block.Skeleton key={index} />
))
: blocks.map((block) => (
<Block
key={block.id}
title={block.name}
description={block.description}
onClick={() => {
addNode(block);
}}
/>
))}
</div>
);
};
export default BlocksList;

View File

@@ -0,0 +1,8 @@
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const InputBlocksContent: React.FC = () => {
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
};
export default InputBlocksContent;

View File

@@ -0,0 +1,112 @@
import { Button } from "@/components/ui/button";
import React, { useState, useEffect, Fragment, useCallback } from "react";
import IntegrationBlock from "../IntegrationBlock";
import { useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Block } from "@/lib/autogpt-server-api";
import ErrorState from "../ErrorState";
import { Skeleton } from "@/components/ui/skeleton";
const IntegrationBlocks: React.FC = ({}) => {
const { integration, setIntegration, addNode } = useBlockMenuContext();
const [blocks, setBlocks] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const api = useBackendAPI();
const fetchBlocks = useCallback(async () => {
if (integration) {
try {
setLoading(true);
setError(null);
const response = await api.getBuilderBlocks({ provider: integration });
setBlocks(response.blocks);
} catch (err) {
console.error("Failed to fetch integration blocks:", err);
setError(
err instanceof Error
? err.message
: "Failed to load integration blocks",
);
} finally {
setLoading(false);
}
}
}, [api, integration]);
useEffect(() => {
fetchBlocks();
}, [api, integration, fetchBlocks]);
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{Array.from({ length: 3 }).map((_, blockIndex) => (
<Fragment key={blockIndex}>
{blockIndex > 0 && (
<Skeleton className="my-4 h-[1px] w-full text-zinc-100" />
)}
{[0, 1, 2].map((index) => (
<IntegrationBlock.Skeleton key={`${blockIndex}-${index}`} />
))}
</Fragment>
))}
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load integration blocks"
error={error}
onRetry={fetchBlocks}
/>
</div>
);
}
return (
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Button
variant={"link"}
className="p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800"
onClick={() => {
setIntegration(null);
}}
>
Integrations
</Button>
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
/
</p>
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
{integration}
</p>
</div>
<span className="flex h-[1.375rem] w-[1.6875rem] items-center justify-center rounded-[1.25rem] bg-[#f0f0f0] p-1.5 font-sans text-sm leading-[1.375rem] text-zinc-500 group-disabled:text-zinc-400">
{blocks.length}
</span>
</div>
<div className="space-y-3">
{blocks.map((block, index) => (
<IntegrationBlock
key={index}
title={block.name}
description={block.description}
icon_url={`/integrations/${integration}.png`}
onClick={() => {
addNode(block);
}}
/>
))}
</div>
</div>
);
};
export default IntegrationBlocks;

View File

@@ -0,0 +1,22 @@
import React from "react";
import PaginatedIntegrationList from "./PaginatedIntegrationList";
import IntegrationBlocks from "./IntegrationBlocks";
import { useBlockMenuContext } from "../block-menu-provider";
const IntegrationsContent: React.FC = () => {
const { integration } = useBlockMenuContext();
if (!integration) {
return <PaginatedIntegrationList />;
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full px-4 pb-4">
<IntegrationBlocks />
</div>
</div>
);
};
export default IntegrationsContent;

View File

@@ -0,0 +1,84 @@
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
import { useBlockMenuContext } from "../block-menu-provider";
const MarketplaceAgentsContent: React.FC = () => {
const {
data: agents,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "store-agents" },
pageSize: 10,
});
const { handleAddStoreAgent, loadingSlug } = useBlockMenuContext();
if (loading) {
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 5 }).map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={index} />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load marketplace agents"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{agents.map((agent) => (
<MarketplaceAgentBlock
key={agent.slug}
slug={agent.slug}
title={agent.agent_name}
image_url={agent.agent_image}
creator_name={agent.creator}
number_of_runs={agent.runs}
loading={loadingSlug === agent.slug}
onClick={() =>
handleAddStoreAgent({
creator_name: agent.creator,
slug: agent.slug,
})
}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);
};
export default MarketplaceAgentsContent;

View File

@@ -0,0 +1,81 @@
import React from "react";
import UGCAgentBlock from "../UGCAgentBlock";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
import { useBlockMenuContext } from "../block-menu-provider";
import { convertLibraryAgentIntoBlock } from "@/lib/utils";
const MyAgentsContent: React.FC = () => {
const {
data: agents,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "library-agents" },
pageSize: 10,
});
const { addNode } = useBlockMenuContext();
if (loading) {
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 5 }).map((_, index) => (
<UGCAgentBlock.Skeleton key={index} />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load library agents"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{agents.map((agent) => (
<UGCAgentBlock
key={agent.id}
title={agent.name}
edited_time={agent.updated_at}
version={agent.graph_version}
image_url={agent.image_url}
onClick={() => {
const block = convertLibraryAgentIntoBlock(agent);
addNode(block);
}}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<UGCAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);
};
export default MyAgentsContent;

View File

@@ -0,0 +1,8 @@
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const OutputBlocksContent: React.FC = () => {
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
};
export default OutputBlocksContent;

View File

@@ -0,0 +1,59 @@
import React, { Fragment } from "react";
import BlocksList from "./BlocksList";
import Block from "../Block";
import { BlockRequest } from "@/lib/autogpt-server-api";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
interface PaginatedBlocksContentProps {
blockRequest: BlockRequest;
pageSize?: number;
}
const PaginatedBlocksContent: React.FC<PaginatedBlocksContentProps> = ({
blockRequest,
pageSize = 10,
}) => {
const {
data: blocks,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "blocks", ...blockRequest },
pageSize,
});
if (error) {
return (
<div className="h-full w-full px-4 pb-4">
<ErrorState
title="Failed to load blocks"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<BlocksList blocks={blocks} loading={loading} />
{loadingMore && hasMore && (
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 3 }).map((_, index) => (
<Block.Skeleton key={`loading-${index}`} />
))}
</div>
)}
</div>
);
};
export default PaginatedBlocksContent;

View File

@@ -0,0 +1,79 @@
import React from "react";
import Integration from "../Integration";
import { useBlockMenuContext } from "../block-menu-provider";
import { usePagination } from "@/hooks/usePagination";
import ErrorState from "../ErrorState";
const PaginatedIntegrationList: React.FC = () => {
const { setIntegration } = useBlockMenuContext();
const {
data: providers,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
} = usePagination({
request: { apiType: "providers" },
pageSize: 10,
});
if (loading) {
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 6 }).map((_, integrationIndex) => (
<Integration.Skeleton key={integrationIndex} />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load integrations"
error={error}
onRetry={refresh}
/>
</div>
);
}
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
<div className="w-full px-4 pb-4">
<div className="space-y-3">
{providers.map((integration, index) => (
<Integration
key={index}
title={integration.name}
icon_url={`/integrations/${integration.name}.png`}
description={integration.description}
number_of_blocks={integration.integration_count}
onClick={() => setIntegration(integration.name)}
/>
))}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<Integration.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
</div>
);
};
export default PaginatedIntegrationList;

View File

@@ -0,0 +1,160 @@
import React, { useCallback, useEffect, useState } from "react";
import SearchHistoryChip from "../SearchHistoryChip";
import IntegrationChip from "../IntegrationChip";
import Block from "../Block";
import { useBlockMenuContext } from "../block-menu-provider";
import {
CredentialsProviderName,
SuggestionsResponse,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import ErrorState from "../ErrorState";
const SuggestionContent: React.FC = () => {
const { setIntegration, setDefaultState, setSearchQuery, addNode } =
useBlockMenuContext();
const [suggestionsData, setSuggestionsData] =
useState<SuggestionsResponse | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const api = useBackendAPI();
const fetchSuggestions = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await api.getSuggestions();
setSuggestionsData(response);
} catch (err) {
console.error("Error fetching data:", err);
setError(
err instanceof Error ? err.message : "Failed to load suggestions",
);
} finally {
setLoading(false);
}
}, [api]);
useEffect(() => {
fetchSuggestions();
}, [api, fetchSuggestions]);
if (error) {
return (
<div className="h-full p-4">
<ErrorState
title="Failed to load suggestions"
error={error}
onRetry={fetchSuggestions}
/>
</div>
);
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
<div className="w-full space-y-6 pb-4">
{/* Recent Searches */}
{/* <div className="-mb-2 space-y-2.5">
<p className="px-4 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Recent searches
</p>
<div className="scrollbar-thumb-rounded flex flex-nowrap gap-2 overflow-x-auto transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200">
{" "}
{!loading && suggestionsData
? suggestionsData.recent_searches.map((search, index) => (
<SearchHistoryChip
key={`search-${index}`}
content={search}
className={index === 0 ? "ml-4" : ""}
onClick={() => setSearchQuery(search)}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<SearchHistoryChip.Skeleton
key={`search-${index}`}
className={index === 0 ? "ml-4" : ""}
/>
))}
{!loading && suggestionsData
? suggestionsData.recent_searches.map((search, index) => (
<SearchHistoryChip
key={`search-${index}`}
content={search}
className={index === 0 ? "ml-4" : ""}
onClick={() => setSearchQuery(search)}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<SearchHistoryChip.Skeleton
key={`search-${index}`}
className={index === 0 ? "ml-4" : ""}
/>
))}
</div>
</div> */}
{/* Integrations */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Integrations
</p>
<div className="grid grid-cols-3 grid-rows-2 gap-2">
{!loading && suggestionsData
? suggestionsData.providers.map((provider, index) => (
<IntegrationChip
key={`integration-${index}`}
icon_url={`/integrations/${provider}.png`}
name={provider}
onClick={() => {
setDefaultState("integrations");
setIntegration(provider as CredentialsProviderName);
}}
/>
))
: Array(6)
.fill(0)
.map((_, index) => (
<IntegrationChip.Skeleton
key={`integration-skeleton-${index}`}
/>
))}
</div>
</div>
{/* Top blocks */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Top blocks
</p>
<div className="space-y-2">
{!loading && suggestionsData
? suggestionsData.top_blocks.map((block, index) => (
<Block
key={`block-${index}`}
title={block.name}
description={block.description}
onClick={() => {
addNode(block);
}}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<Block.Skeleton key={`block-skeleton-${index}`} />
))}
</div>
</div>
</div>
</div>
);
};
export default SuggestionContent;

View File

@@ -0,0 +1,64 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Plus } from "lucide-react";
import { ButtonHTMLAttributes } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;
description?: string;
ai_name?: string;
}
const AiBlock: React.FC<Props> = ({
title,
description,
className,
ai_name,
...rest
}) => {
return (
<Button
className={cn(
"group flex h-[5.625rem] w-full min-w-[7.5rem] items-center justify-start space-x-3 whitespace-normal rounded-[0.75rem] bg-zinc-50 px-[0.875rem] py-[0.625rem] text-start shadow-none",
"hover:bg-zinc-100 focus:ring-0 active:bg-zinc-100 active:ring-1 active:ring-zinc-300 disabled:pointer-events-none",
)}
{...rest}
>
<div className="flex flex-1 flex-col items-start gap-1.5">
<div className="space-y-0.5">
<span
className={cn(
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-700 group-disabled:text-zinc-400",
)}
>
{title}
</span>
<span
className={cn(
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
)}
>
{description}
</span>
</div>
<span
className={cn(
"rounded-[0.75rem] bg-zinc-200 px-[0.5rem] font-sans text-xs leading-[1.25rem] text-zinc-500",
)}
>
Supports {ai_name}
</span>
</div>
<div
className={cn(
"flex h-7 w-7 items-center justify-center rounded-[0.5rem] bg-zinc-700 group-disabled:bg-zinc-400",
)}
>
<Plus className="h-5 w-5 text-zinc-50" strokeWidth={2} />
</div>
</Button>
);
};
export default AiBlock;

View File

@@ -0,0 +1,147 @@
import React, { useEffect, useState, useCallback, useRef } from "react";
import FiltersList from "./FiltersList";
import SearchList from "./SearchList";
import { useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const BlockMenuSearch: React.FC = ({}) => {
const {
searchData,
searchQuery,
searchId,
setSearchData,
filters,
setCategoryCounts,
} = useBlockMenuContext();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(true);
const [page, setPage] = useState<number>(1);
const [loadingMore, setLoadingMore] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const api = useBackendAPI();
const pageSize = 10;
const fetchSearchData = useCallback(
async (pageNum: number, isLoadMore: boolean = false) => {
if (isLoadMore) {
setLoadingMore(true);
} else {
setIsLoading(true);
}
try {
const activeCategories = Object.entries(filters.categories)
.filter(([_, isActive]) => isActive)
.map(([category, _]) => category)
.map(
(category) =>
category as
| "blocks"
| "integrations"
| "marketplace_agents"
| "my_agents",
);
const response = await api.searchBlocks({
search_query: searchQuery,
search_id: searchId,
page: pageNum,
page_size: pageSize,
filter: activeCategories.length > 0 ? activeCategories : undefined,
by_creator:
filters.createdBy.length > 0 ? filters.createdBy : undefined,
});
setCategoryCounts(response.total_items);
if (isLoadMore) {
setSearchData((prev) => [...prev, ...response.items]);
} else {
setSearchData(response.items);
}
setHasMore(response.more_pages);
setError(null);
} catch (error) {
console.error("Error fetching search data:", error);
setError(
error instanceof Error
? error.message
: "Failed to load search results",
);
if (!isLoadMore) {
setPage(1);
}
} finally {
setIsLoading(false);
setLoadingMore(false);
}
},
[
searchQuery,
searchId,
filters,
api,
setCategoryCounts,
setSearchData,
pageSize,
],
);
const handleScroll = useCallback(() => {
if (!scrollRef.current || loadingMore || !hasMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
if (scrollTop + clientHeight >= scrollHeight - 100) {
const nextPage = page + 1;
setPage(nextPage);
fetchSearchData(nextPage, true);
}
}, [loadingMore, hasMore, page, fetchSearchData]);
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement) {
scrollElement.addEventListener("scroll", handleScroll);
return () => scrollElement.removeEventListener("scroll", handleScroll);
}
}, [handleScroll]);
useEffect(() => {
if (searchQuery) {
setPage(1);
setHasMore(true);
setError(null);
fetchSearchData(1, false);
} else {
setSearchData([]);
setError(null);
setPage(1);
setHasMore(true);
}
}, [searchQuery, searchId, filters, fetchSearchData, setSearchData]);
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full space-y-4 overflow-y-auto py-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200"
>
{searchData.length !== 0 && <FiltersList />}
<SearchList
isLoading={isLoading}
loadingMore={loadingMore}
hasMore={hasMore}
error={error}
onRetry={() => {
setPage(1);
setError(null);
fetchSearchData(1, false);
}}
/>
</div>
);
};
export default BlockMenuSearch;

View File

@@ -0,0 +1,250 @@
import FilterChip from "../FilterChip";
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { cn, getBlockType } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import {
CategoryKey,
Filters,
useBlockMenuContext,
} from "../block-menu-provider";
import { StoreAgent } from "@/lib/autogpt-server-api";
export default function FilterSheet({
categories,
}: {
categories: Array<{ key: CategoryKey; name: string }>;
}) {
const { filters, setFilters, searchData } = useBlockMenuContext();
const [isOpen, setIsOpen] = useState(false);
const [isSheetVisible, setIsSheetVisible] = useState(false);
const [localFilters, setLocalFilters] = useState<Filters>(filters);
const [creators, setCreators] = useState<string[]>([]);
useEffect(() => {
if (isOpen) {
setIsSheetVisible(true);
setLocalFilters(filters);
const marketplaceAgents = (searchData?.filter(
(item) => getBlockType(item) === "store_agent",
) || []) as StoreAgent[];
const uniqueCreators = Array.from(
new Set(marketplaceAgents.map((agent) => agent.creator)),
);
setCreators(uniqueCreators);
} else {
const timer = setTimeout(() => {
setIsSheetVisible(false);
}, 300);
return () => clearTimeout(timer);
}
}, [isOpen, filters, searchData]);
const onCategoryChange = useCallback((category: CategoryKey) => {
setLocalFilters((prev) => ({
...prev,
categories: {
...prev.categories,
[category]: !prev.categories[category],
},
}));
}, []);
const onCreatorChange = useCallback((creator: string) => {
setLocalFilters((prev) => {
const updatedCreators = prev.createdBy.includes(creator)
? prev.createdBy.filter((c) => c !== creator)
: [...prev.createdBy, creator];
return {
...prev,
createdBy: updatedCreators,
};
});
}, []);
const handleApplyFilters = useCallback(() => {
setFilters(localFilters);
setIsOpen(false);
}, [localFilters, setFilters]);
const handleClearFilters = useCallback(() => {
const clearedFilters: Filters = {
categories: {
blocks: false,
integrations: false,
marketplace_agents: false,
my_agents: false,
providers: false,
},
createdBy: [],
};
setFilters(clearedFilters);
setIsOpen(false);
}, [setFilters]);
const hasLocalActiveFilters = useCallback(() => {
const hasCategoryFilter = Object.values(localFilters.categories).some(
(value) => value,
);
const hasCreatorFilter = localFilters.createdBy.length > 0;
return hasCategoryFilter || hasCreatorFilter;
}, [localFilters]);
const hasActiveFilters = useCallback(() => {
const hasCategoryFilter = Object.values(filters.categories).some(
(value) => value,
);
const hasCreatorFilter = filters.createdBy.length > 0;
return hasCategoryFilter || hasCreatorFilter;
}, [filters]);
return (
<div className="m-0 ml-4 inline w-fit p-0">
<Button
onClick={() => {
setIsSheetVisible(true);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsOpen(true);
});
});
}}
variant={"link"}
className="m-0 p-0 hover:no-underline"
>
<FilterChip
name={hasActiveFilters() ? "Edit filters" : "All filters"}
/>
</Button>
{isSheetVisible && (
<>
<div
className={cn(
"absolute bottom-2 left-2 top-2 z-20 w-3/4 max-w-[22.5rem] space-y-4 overflow-hidden rounded-[0.75rem] bg-white pb-4 shadow-[0_4px_12px_2px_rgba(0,0,0,0.1)] transition-all",
isOpen
? "translate-x-0 duration-300 ease-out"
: "-translate-x-full duration-300 ease-out",
)}
>
<div
className={cn(
"flex-1 space-y-4 pb-16",
"scrollbar-thumb-rounded h-full overflow-y-auto pt-4 transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-transparent hover:scrollbar-thumb-zinc-200",
)}
>
{/* Top */}
<div className="flex items-center justify-between px-5">
<p className="font-sans text-base text-[#040404]">Filters</p>
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(false)}
>
<X className="h-5 w-5" />
</Button>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Categories */}
<div className="space-y-4 px-5">
<p className="font-sans text-base font-medium text-zinc-800">
Categories
</p>
<div className="space-y-2">
{categories.map((category) => (
<div
key={category.key}
className="flex items-center space-x-2"
>
<Checkbox
id={category.key}
checked={localFilters.categories[category.key]}
onCheckedChange={() => onCategoryChange(category.key)}
className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white"
/>
<label
htmlFor={category.key}
className="font-sans text-sm leading-[1.375rem] text-zinc-600"
>
{category.name}
</label>
</div>
))}
</div>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Created By */}
<div className="space-y-4 px-5">
<p className="font-sans text-base font-medium text-zinc-800">
Created by
</p>
<div className="space-y-2">
{creators.map((creator) => (
<div key={creator} className="flex items-center space-x-2">
<Checkbox
id={`creator-${creator}`}
checked={localFilters.createdBy.includes(creator)}
onCheckedChange={() => onCreatorChange(creator)}
className="border border-[#D4D4D4] shadow-none data-[state=checked]:border-none data-[state=checked]:bg-violet-700 data-[state=checked]:text-white"
/>
<label
htmlFor={`creator-${creator}`}
className="font-sans text-sm leading-[1.375rem] text-zinc-600"
>
{creator}
</label>
</div>
))}
</div>
{creators.length > 5 && (
<Button
variant={"link"}
className="m-0 p-0 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 underline hover:text-zinc-600"
>
More
</Button>
)}
</div>
</div>
{/* Footer buttons */}
<div className="fixed bottom-0 flex w-full justify-between gap-3 border-t border-zinc-300 bg-white px-5 py-3">
<Button
className="min-w-[5rem] rounded-[0.5rem] border-none px-1.5 py-2 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 shadow-none ring-1 ring-zinc-400"
variant={"outline"}
onClick={handleClearFilters}
>
Clear
</Button>
<Button
className={cn(
"min-w-[6.25rem] rounded-[0.5rem] border-none px-1.5 py-2 font-sans text-sm font-medium leading-[1.375rem] text-white shadow-none ring-1 disabled:ring-0",
)}
onClick={handleApplyFilters}
disabled={!hasLocalActiveFilters()}
>
Apply filters
</Button>
</div>
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { useCallback } from "react";
import FilterChip from "../FilterChip";
import FilterSheet from "./FilterSheet";
import { CategoryKey, useBlockMenuContext } from "../block-menu-provider";
const FiltersList = () => {
const { filters, setFilters, categoryCounts } = useBlockMenuContext();
const categories: Array<{ key: CategoryKey; name: string }> = [
{ key: "blocks", name: "Blocks" },
{ key: "integrations", name: "Integrations" },
{ key: "marketplace_agents", name: "Marketplace agents" },
{ key: "my_agents", name: "My agents" },
];
const handleCategoryFilter = (category: CategoryKey) => {
setFilters({
...filters,
categories: {
...filters.categories,
[category]: !filters.categories[category],
},
});
};
const handleCreatorFilter = useCallback(
(creator: string) => {
const updatedCreators = filters.createdBy.includes(creator)
? filters.createdBy.filter((c) => c !== creator)
: [...filters.createdBy, creator];
setFilters({
...filters,
createdBy: updatedCreators,
});
},
[filters, setFilters],
);
return (
<div className="flex flex-nowrap gap-3 overflow-x-auto scrollbar-hide">
<FilterSheet categories={categories} />
{filters.createdBy.map((creator) => (
<FilterChip
key={creator}
name={"Created by " + creator}
selected={true}
onClick={() => handleCreatorFilter(creator)}
/>
))}
{categories.map((category) => (
<FilterChip
key={category.key}
name={category.name}
number={categoryCounts[category.key]}
selected={filters.categories[category.key]}
onClick={() => handleCategoryFilter(category.key)}
/>
))}
</div>
);
};
export default FiltersList;

View File

@@ -0,0 +1,19 @@
import { Frown } from "lucide-react";
const NoSearchResult = () => {
return (
<div className="flex h-full w-full flex-col items-center justify-center text-center">
<Frown className="mb-10 h-16 w-16 text-zinc-400" strokeWidth={1} />
<div className="space-y-1">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
No match found
</p>
<p className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-600">
Try adjusting your search terms
</p>
</div>
</div>
);
};
export default NoSearchResult;

View File

@@ -0,0 +1,173 @@
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import Block from "../Block";
import UGCAgentBlock from "../UGCAgentBlock";
import AiBlock from "./AiBlock";
import IntegrationBlock from "../IntegrationBlock";
import { useBlockMenuContext } from "../block-menu-provider";
import NoSearchResult from "./NoSearchResult";
import { Button } from "@/components/ui/button";
import { convertLibraryAgentIntoBlock, getBlockType } from "@/lib/utils";
interface SearchListProps {
isLoading: boolean;
loadingMore: boolean;
hasMore: boolean;
error: string | null;
onRetry: () => void;
}
const SearchList: React.FC<SearchListProps> = ({
isLoading,
loadingMore,
hasMore,
error,
onRetry,
}) => {
const { searchQuery, addNode, loadingSlug, searchData, handleAddStoreAgent } =
useBlockMenuContext();
if (isLoading) {
return (
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Search results
</p>
{Array(6)
.fill(0)
.map((_, i) => (
<Block.Skeleton key={i} />
))}
</div>
);
}
if (error) {
return (
<div className="px-4">
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="mb-2 text-sm text-red-600">
Error loading search results: {error}
</p>
<Button
variant="outline"
size="sm"
onClick={onRetry}
className="h-7 text-xs"
>
Retry
</Button>
</div>
</div>
);
}
if (searchData.length === 0) {
return <NoSearchResult />;
}
return (
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Search results
</p>
{searchData.map((item: any, index: number) => {
const blockType = getBlockType(item);
switch (blockType) {
case "store_agent":
return (
<MarketplaceAgentBlock
key={index}
slug={item.slug}
highlightedText={searchQuery}
title={item.agent_name}
image_url={item.agent_image}
creator_name={item.creator}
number_of_runs={item.runs}
loading={loadingSlug == item.slug}
onClick={() =>
handleAddStoreAgent({
creator_name: item.creator,
slug: item.slug,
})
}
/>
);
case "block":
return (
<Block
key={index}
title={item.name}
highlightedText={searchQuery}
description={item.description}
onClick={() => {
addNode(item);
}}
/>
);
case "provider":
return (
<IntegrationBlock
key={index}
title={item.name}
highlightedText={searchQuery}
icon_url={`/integrations/${item.name}.png`}
description={item.description}
onClick={() => {
addNode(item);
}}
/>
);
case "library_agent":
return (
<UGCAgentBlock
key={index}
title={item.name}
highlightedText={searchQuery}
image_url={item.image_url}
version={item.graph_version}
edited_time={item.updated_at}
onClick={() => {
const block = convertLibraryAgentIntoBlock(item);
addNode(block);
}}
/>
);
case "ai_agent":
return (
<AiBlock
key={index}
title={item.name}
description={item.description}
ai_name={item.inputSchema.properties.model.enum.find(
(model: string) =>
model
.toLowerCase()
.includes(searchQuery.toLowerCase().trim()),
)}
onClick={() => {
const block = convertLibraryAgentIntoBlock(item);
addNode(block);
}}
/>
);
default:
return null;
}
})}
{loadingMore && hasMore && (
<div className="space-y-2.5">
{Array(3)
.fill(0)
.map((_, i) => (
<Block.Skeleton key={`loading-more-${i}`} />
))}
</div>
)}
</div>
);
};
export default SearchList;

View File

@@ -1,13 +1,7 @@
import { Card, CardContent } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import React from "react";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
/**
* Represents a control element for the ControlPanel Component.
@@ -27,6 +21,7 @@ interface ControlPanelProps {
controls: Control[];
topChildren?: React.ReactNode;
botChildren?: React.ReactNode;
className?: string;
}
@@ -45,42 +40,31 @@ export const ControlPanel = ({
className,
}: ControlPanelProps) => {
return (
<Card className={cn("m-4 mt-24 w-14 dark:bg-slate-900", className)}>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-3 rounded-xl py-3">
{topChildren}
<Separator className="dark:bg-slate-700" />
{controls.map((control, index) => (
<Tooltip key={index} delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Button
variant="ghost"
size="icon"
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</div>
</TooltipTrigger>
<TooltipContent
side="right"
className="dark:bg-slate-800 dark:text-slate-100"
>
{control.label}
</TooltipContent>
</Tooltip>
))}
<Separator className="dark:bg-slate-700" />
{botChildren}
</div>
</CardContent>
</Card>
<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,
)}
>
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
{topChildren}
<Separator className="text-[#E1E1E1]" />
{controls.map((control, index) => (
<ControlPanelButton
key={index}
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="rounded-none"
>
{control.icon}
</ControlPanelButton>
))}
<Separator className="text-[#E1E1E1]" />
{botChildren}
</div>
</section>
);
};
export default ControlPanel;

View File

@@ -16,6 +16,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
interface SaveControlProps {
agentMeta: GraphMeta | null;
@@ -26,6 +27,11 @@ interface SaveControlProps {
onNameChange: (name: string) => void;
onDescriptionChange: (description: string) => void;
pinSavePopover: boolean;
blockMenuSelected: "save" | "block" | "";
setBlockMenuSelected: React.Dispatch<
React.SetStateAction<"" | "save" | "block">
>;
}
/**
@@ -48,6 +54,8 @@ export const SaveControl = ({
onNameChange,
agentDescription,
onDescriptionChange,
blockMenuSelected,
setBlockMenuSelected,
pinSavePopover,
}: SaveControlProps) => {
/**
@@ -82,27 +90,29 @@ export const SaveControl = ({
}, [handleSave, toast]);
return (
<Popover open={pinSavePopover ? true : undefined}>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
data-id="save-control-popover-trigger"
data-testid="blocks-control-save-button"
name="Save"
>
<IconSave className="dark:text-gray-300" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Save</TooltipContent>
</Tooltip>
<Popover
open={pinSavePopover ? true : undefined}
onOpenChange={(open) => open || setBlockMenuSelected("")}
>
<PopoverTrigger>
<ControlPanelButton
data-id="save-control-popover-trigger"
data-testid="blocks-control-save-button"
selected={blockMenuSelected === "save"}
onClick={() => {
setBlockMenuSelected("save");
}}
className="rounded-none"
>
<IconSave className="h-5 w-5" strokeWidth={2} />
</ControlPanelButton>
</PopoverTrigger>
<PopoverContent
side="right"
sideOffset={15}
sideOffset={16}
align="start"
className="w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
data-id="save-control-popover-content"
>
<Card className="border-none shadow-none dark:bg-slate-900">

View File

@@ -5,6 +5,7 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -13,7 +14,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 border-neutral-900 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-50 dark:border-neutral-800 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-900 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-50 dark:border-neutral-800 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
className,
)}
{...props}
@@ -21,7 +22,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
<CheckIcon className="h-4 w-4" strokeWidth={2} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));

View File

@@ -256,7 +256,7 @@ const MultiSelectorList = forwardRef<
<CommandList
ref={ref}
className={cn(
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors",
"scrollbar-thumb-rounded-lg absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted",
className,
)}
>

View File

@@ -0,0 +1 @@
export { usePagination } from "./usePagination";

View File

@@ -10,6 +10,7 @@ import BackendAPI, {
GraphID,
NodeExecutionResult,
SpecialBlockID,
Node,
} from "@/lib/autogpt-server-api";
import {
deepEquals,
@@ -177,6 +178,16 @@ export default function useAgentGraph(
setAgentName(graph.name);
setAgentDescription(graph.description);
const getGraphName = (node: Node) => {
if (node.input_default.agent_name) {
return node.input_default.agent_name;
}
return (
availableFlows.find((flow) => flow.id === node.input_default.graph_id)
?.name || null
);
};
setNodes((prevNodes) => {
const _newNodes = graph.nodes.map((node) => {
const block = availableNodes.find(
@@ -184,12 +195,8 @@ export default function useAgentGraph(
)!;
if (!block) return null;
const prevNode = prevNodes.find((n) => n.id === node.id);
const flow =
block.uiType == BlockUIType.AGENT
? availableFlows.find(
(flow) => flow.id === node.input_default.graph_id,
)
: null;
const graphName =
(block.uiType == BlockUIType.AGENT && getGraphName(node)) || null;
const newNode: CustomNode = {
id: node.id,
type: "custom",
@@ -201,7 +208,7 @@ export default function useAgentGraph(
isOutputOpen: false,
...prevNode?.data,
block_id: block.id,
blockType: flow?.name || block.name,
blockType: graphName || block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
@@ -284,15 +291,17 @@ export default function useAgentGraph(
const getToolFuncName = (nodeId: string) => {
const sinkNode = nodes.find((node) => node.id === nodeId);
const sinkNodeName = sinkNode
? sinkNode.data.block_id === SpecialBlockID.AGENT
? sinkNode.data.hardcodedValues?.graph_id
? availableFlows.find(
(flow) => flow.id === sinkNode.data.hardcodedValues.graph_id,
)?.name || "agentexecutorblock"
: "agentexecutorblock"
: sinkNode.data.title.split(" ")[0]
: "";
if (!sinkNode) return "";
const sinkNodeName =
sinkNode.data.block_id === SpecialBlockID.AGENT
? sinkNode.data.hardcodedValues?.agent_name ||
availableFlows.find(
(flow) => flow.id === sinkNode.data.hardcodedValues.graph_id,
)?.name ||
"agentexecutorblock"
: sinkNode.data.title.split(" ")[0];
return sinkNodeName;
};
@@ -1116,7 +1125,6 @@ export default function useAgentGraph(
setAgentDescription,
savedAgent,
availableNodes,
availableFlows,
getOutputType,
requestSave,
requestSaveAndRun,

View File

@@ -0,0 +1,232 @@
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Block,
BlockRequest,
Provider,
StoreAgent,
LibraryAgent,
LibraryAgentSortEnum,
} from "@/lib/autogpt-server-api";
type BlocksPaginationRequest = { apiType: "blocks" } & BlockRequest;
type ProvidersPaginationRequest = { apiType: "providers" } & {
page?: number;
page_size?: number;
};
type StoreAgentsPaginationRequest = { apiType: "store-agents" } & {
featured?: boolean;
creator?: string;
sorted_by?: string;
search_query?: string;
category?: string;
page?: number;
page_size?: number;
};
type LibraryAgentsPaginationRequest = { apiType: "library-agents" } & {
search_term?: string;
sort_by?: LibraryAgentSortEnum;
page?: number;
page_size?: number;
};
type PaginationRequest =
| BlocksPaginationRequest
| ProvidersPaginationRequest
| StoreAgentsPaginationRequest
| LibraryAgentsPaginationRequest;
interface UsePaginationOptions<T extends PaginationRequest> {
request: T;
pageSize?: number;
enabled?: boolean;
}
interface UsePaginationReturn<T> {
data: T[];
loading: boolean;
loadingMore: boolean;
hasMore: boolean;
error: string | null;
scrollRef: React.RefObject<HTMLDivElement>;
refresh: () => void;
loadMore: () => void;
}
type GetReturnType<T> = T extends BlocksPaginationRequest
? Block
: T extends ProvidersPaginationRequest
? Provider
: T extends StoreAgentsPaginationRequest
? StoreAgent
: T extends LibraryAgentsPaginationRequest
? LibraryAgent
: never;
export const usePagination = <T extends PaginationRequest>({
request,
pageSize = 10,
enabled = true, // to allow pagination or not
}: UsePaginationOptions<T>): UsePaginationReturn<GetReturnType<T>> => {
const [data, setData] = useState<GetReturnType<T>[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [error, setError] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const isLoadingRef = useRef(false);
const requestRef = useRef(request);
const api = useBackendAPI();
// because we are using this pagination for multiple components
requestRef.current = request;
const fetchData = useCallback(
async (page: number, isLoadMore = false) => {
if (isLoadingRef.current || !enabled) return;
isLoadingRef.current = true;
if (isLoadMore) {
setLoadingMore(true);
} else {
setLoading(true);
}
setError(null);
try {
let response;
let newData: GetReturnType<T>[];
let pagination;
const currentRequest = requestRef.current;
const requestWithPagination = {
...currentRequest,
page,
page_size: pageSize,
};
switch (currentRequest.apiType) {
case "blocks":
const { apiType: _, ...blockRequest } = requestWithPagination;
response = await api.getBuilderBlocks(blockRequest);
newData = response.blocks as GetReturnType<T>[];
pagination = response.pagination;
break;
case "providers":
const { apiType: __, ...providerRequest } = requestWithPagination;
response = await api.getProviders(providerRequest);
newData = response.providers as GetReturnType<T>[];
pagination = response.pagination;
break;
case "store-agents":
const { apiType: ___, ...storeAgentRequest } =
requestWithPagination;
response = await api.getStoreAgents(storeAgentRequest);
newData = response.agents as GetReturnType<T>[];
pagination = response.pagination;
break;
case "library-agents":
const { apiType: ____, ...libraryAgentRequest } =
requestWithPagination;
response = await api.listLibraryAgents(libraryAgentRequest);
newData = response.agents as GetReturnType<T>[];
pagination = response.pagination;
break;
default:
throw new Error(
`Unknown request type: ${(currentRequest as any).apiType}`,
);
}
if (isLoadMore) {
setData((prev) => [...prev, ...newData]);
} else {
setData(newData);
}
setHasMore(page < pagination.total_pages);
setCurrentPage(page);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch data";
setError(errorMessage);
console.error("Error fetching data:", err);
} finally {
setLoading(false);
setLoadingMore(false);
isLoadingRef.current = false;
}
},
[api, pageSize, enabled],
);
const handleScroll = useCallback(() => {
const scrollElement = scrollRef.current;
if (
!scrollElement ||
loadingMore ||
!hasMore ||
isLoadingRef.current ||
!enabled
)
return;
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
const threshold = 100;
if (scrollTop + clientHeight >= scrollHeight - threshold) {
fetchData(currentPage + 1, true);
}
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
const refresh = useCallback(() => {
setCurrentPage(1);
setHasMore(true);
setError(null);
fetchData(1);
}, [fetchData]);
const loadMore = useCallback(() => {
if (!loadingMore && hasMore && !isLoadingRef.current && enabled) {
fetchData(currentPage + 1, true);
}
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
const requestString = JSON.stringify(request);
useEffect(() => {
if (enabled) {
setCurrentPage(1);
setHasMore(true);
setError(null);
setData([]);
fetchData(1);
}
}, [requestString, enabled, fetchData]);
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement && enabled) {
scrollElement.addEventListener("scroll", handleScroll);
return () => scrollElement.removeEventListener("scroll", handleScroll);
}
}, [handleScroll, enabled]);
return {
data,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
loadMore,
};
};

View File

@@ -9,6 +9,11 @@ import type {
APIKeyCredentials,
APIKeyPermission,
Block,
BlockCategoryResponse,
BlockRequest,
BlockResponse,
BlockSearchResponse,
CountResponse,
CreateAPIKeyResponse,
CreateLibraryAgentPresetRequest,
CreatorDetails,
@@ -18,6 +23,7 @@ import type {
CredentialsDeleteResponse,
CredentialsMetaInput,
CredentialsMetaResponse,
CredentialsProviderName,
Graph,
GraphCreatable,
GraphExecution,
@@ -39,6 +45,8 @@ import type {
OttoQuery,
OttoResponse,
ProfileDetails,
Provider,
ProviderResponse,
RefundRequest,
ReviewSubmissionRequest,
Schedule,
@@ -53,12 +61,14 @@ import type {
StoreSubmissionRequest,
StoreSubmissionsResponse,
SubmissionStatus,
SuggestionsResponse,
TransactionHistory,
User,
UserOnboarding,
UserPasswordCredentials,
UsersBalanceHistoryResponse,
} from "./types";
import { DefaultStateType } from "@/components/builder/block-menu/block-menu-provider";
const isClient = typeof window !== "undefined";
@@ -202,6 +212,44 @@ export default class BackendAPI {
return this._get("/onboarding/enabled");
}
////////////////////////////////////////
//////////////// BUILDER ///////////////
////////////////////////////////////////
getSuggestions(): Promise<SuggestionsResponse> {
return this._get("/builder/suggestions");
}
getBlockCategories(): Promise<BlockCategoryResponse[]> {
return this._get("/builder/categories");
}
getBuilderBlocks(request?: BlockRequest): Promise<BlockResponse> {
return this._get("/builder/blocks", request);
}
getProviders(request?: {
page?: number;
page_size?: number;
}): Promise<ProviderResponse> {
return this._get("/builder/providers", request);
}
searchBlocks(options: {
search_query?: string;
filter?: ("blocks" | "integrations" | "marketplace_agents" | "my_agents")[];
by_creator?: string[];
search_id?: string;
page?: number;
page_size?: number;
}): Promise<BlockSearchResponse> {
return this._request("POST", "/builder/search", options);
}
getBlockCounts(): Promise<CountResponse> {
return this._get("/builder/counts");
}
////////////////////////////////////////
//////////////// GRAPHS ////////////////
////////////////////////////////////////

View File

@@ -27,6 +27,71 @@ export type BlockCost = {
cost_filter: { [key: string]: any };
};
/* Mirror of backend/server/v2/builder/model.py:SuggestionsResponse */
export type SuggestionsResponse = {
otto_suggestions: string[];
recent_searches: string[];
providers: string[];
top_blocks: Block[];
};
/* Mirror of backend/server/v2/builder/model.py:BlockCategoryResponse */
export type BlockCategoryResponse = {
name: string;
total_blocks: number;
blocks: Block[];
};
export type BlockRequest = {
page?: number;
page_size?: number;
} & (
| { category?: string }
| { type?: "all" | "input" | "action" | "output" }
| { provider?: CredentialsProviderName }
);
/* Mirror of backend/server/v2/builder/model.py:BlockReponse */
export type BlockResponse = {
blocks: Block[];
pagination: Pagination;
};
/* Mirror of backend/server/v2/builder/model.py:Provider */
export type Provider = {
name: CredentialsProviderName;
description: string;
integration_count: number;
};
/* Mirror of backend/server/v2/builder/model.py:ProviderResponse */
export type ProviderResponse = {
providers: Provider[];
pagination: Pagination;
};
/* Mirror of backend/server/v2/builder/model.py:BlockSearchResponse */
export type BlockSearchResponse = {
items: (Block | LibraryAgent | StoreAgent)[];
total_items: Record<
"blocks" | "integrations" | "marketplace_agents" | "my_agents",
number
>;
page: number;
more_pages: boolean;
};
/* Mirror of backend/server/v2/builder/model.py:CountResponse */
export type CountResponse = {
all_blocks: number;
input_blocks: number;
action_blocks: number;
output_blocks: number;
integrations: number;
marketplace_agents: number;
my_agents: number;
};
/* Mirror of backend/data/block.py:Block */
export type Block = {
id: string;
@@ -400,6 +465,7 @@ export type LibraryAgent = {
name: string;
description: string;
input_schema: BlockIOObjectSubSchema;
output_schema: BlockIOObjectSubSchema;
new_output: boolean;
can_access_graph: boolean;
is_latest_version: boolean;

View File

@@ -1,7 +1,14 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Category, Graph } from "@/lib/autogpt-server-api/types";
import {
Block,
BlockUIType,
Category,
Graph,
LibraryAgent,
SpecialBlockID,
} from "@/lib/autogpt-server-api/types";
import { NodeDimension } from "@/components/Flow";
export function cn(...inputs: ClassValue[]) {
@@ -396,3 +403,50 @@ export function getValue(key: string, value: any) {
export function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0;
}
export const convertLibraryAgentIntoBlock = (agent: LibraryAgent) => {
const block = {
id: SpecialBlockID.AGENT,
name: agent.name,
description:
`Ver.${agent.graph_version}` +
(agent.description ? ` | ${agent.description}` : ""),
categories: [{ category: "AGENT", description: "" }],
inputSchema: agent.input_schema,
outputSchema: agent.output_schema,
staticOutput: false,
uiType: BlockUIType.AGENT,
uiKey: agent.id,
costs: [],
hardcodedValues: {
graph_id: agent.graph_id,
graph_version: agent.graph_version,
input_schema: agent.input_schema,
output_schema: agent.output_schema,
agent_name: agent.name,
},
} as Block;
return block;
};
// Need to change it once, we got provider blocks
export const getBlockType = (item: any) => {
if (item?.inputSchema?.properties?.model?.title === "LLM Model") {
return "ai_agent";
}
if (item.id && item.name && item.inputSchema && item.outputSchema) {
return "block";
}
if (item.name && typeof item.integration_count === "number") {
return "provider";
}
if (item.id && item.graph_id && item.status) {
return "library_agent";
}
if (item.slug && item.agent_name && item.runs !== undefined) {
return "store_agent";
}
return null;
};

View File

@@ -1,4 +1,5 @@
import type { Config } from "tailwindcss";
import scrollbarHide from "tailwind-scrollbar-hide";
const config = {
darkMode: ["class"],
@@ -142,7 +143,11 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [
require("tailwindcss-animate"),
scrollbarHide,
require("tailwind-scrollbar"),
],
} satisfies Config;
export default config;