Compare commits
127 Commits
native-aut
...
redesignin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f917327b2 | ||
|
|
53c92be68f | ||
|
|
7d0c4b9f7f | ||
|
|
8902ecb3ae | ||
|
|
845a6ab38b | ||
|
|
79afd83919 | ||
|
|
be8144b305 | ||
|
|
e63575877d | ||
|
|
9206d24017 | ||
|
|
b2ab2602fe | ||
|
|
aa4de454b2 | ||
|
|
9ea44b6267 | ||
|
|
3cd214d0d4 | ||
|
|
04d30efc5d | ||
|
|
9157388723 | ||
|
|
455f273ccf | ||
|
|
382598f2be | ||
|
|
79b6a56b56 | ||
|
|
68cec8b2e7 | ||
|
|
b921edb062 | ||
|
|
b7408415df | ||
|
|
59752054fa | ||
|
|
478f31141d | ||
|
|
5c264c253c | ||
|
|
d6d4703bbc | ||
|
|
0b602600cb | ||
|
|
19382072b1 | ||
|
|
3e2b388df0 | ||
|
|
a50532a975 | ||
|
|
27e53aa3dd | ||
|
|
a24673d15f | ||
|
|
9d2d9606e8 | ||
|
|
91407dfc33 | ||
|
|
851919d2d5 | ||
|
|
d6acb02cb6 | ||
|
|
9c07206725 | ||
|
|
4bd3447301 | ||
|
|
8adc9f967d | ||
|
|
349b70c4bc | ||
|
|
9ecfa1e1f1 | ||
|
|
4e17f9c49e | ||
|
|
31fdeeb706 | ||
|
|
e42b24c029 | ||
|
|
2d52a57a21 | ||
|
|
f45123f6b6 | ||
|
|
d524518f41 | ||
|
|
81d1b28d92 | ||
|
|
4e4e754ac1 | ||
|
|
e409d7aa34 | ||
|
|
8312a339c2 | ||
|
|
5b45d246ef | ||
|
|
5c7c7ca874 | ||
|
|
c93c5e35ba | ||
|
|
ce989b1bf7 | ||
|
|
c1c919b88b | ||
|
|
21a91fe9fd | ||
|
|
b2f3d8c1f2 | ||
|
|
46ab2e3b20 | ||
|
|
5b40700299 | ||
|
|
1a97020eeb | ||
|
|
39d03f2090 | ||
|
|
8088d294f4 | ||
|
|
31266949ed | ||
|
|
f4eb00a6ad | ||
|
|
f75cc0dd11 | ||
|
|
21b612625f | ||
|
|
eec0d276d5 | ||
|
|
c6941e7f6e | ||
|
|
325684a10f | ||
|
|
cf057cbbda | ||
|
|
f3a7be1fd3 | ||
|
|
97bcb0f95e | ||
|
|
dd71d65706 | ||
|
|
2b2d26bcde | ||
|
|
67f6f43e1b | ||
|
|
a3409c9578 | ||
|
|
7f82457ea4 | ||
|
|
a5c0fabc00 | ||
|
|
09dba93a4a | ||
|
|
ea2cd3e7bf | ||
|
|
d3d0ccf732 | ||
|
|
d8d5d6ec0c | ||
|
|
f45b09c0b5 | ||
|
|
1e89b6d3a4 | ||
|
|
950a85e179 | ||
|
|
c5e3148145 | ||
|
|
a135ba3f0b | ||
|
|
fe95e27226 | ||
|
|
711ca10cc9 | ||
|
|
1346d8230c | ||
|
|
07c84a4757 | ||
|
|
596824c1e7 | ||
|
|
79afa6db99 | ||
|
|
e034c16f31 | ||
|
|
9012eff1ac | ||
|
|
0361ea4aa4 | ||
|
|
6f1c522ea3 | ||
|
|
2d654bf64b | ||
|
|
bb69e32fee | ||
|
|
1be830835b | ||
|
|
a2a4d546f7 | ||
|
|
3053a7bd06 | ||
|
|
bbf4108136 | ||
|
|
95387bcf78 | ||
|
|
e1fc56e6f3 | ||
|
|
2a06956802 | ||
|
|
32231ff80f | ||
|
|
d0b23c085f | ||
|
|
e718d3d3d8 | ||
|
|
1971a62684 | ||
|
|
e125b5923c | ||
|
|
c6942e4e6f | ||
|
|
c9e421a219 | ||
|
|
7868373897 | ||
|
|
f1c8399e0e | ||
|
|
97ba69ef1c | ||
|
|
773e1488bf | ||
|
|
4273be59ba | ||
|
|
06e524788a | ||
|
|
bc08012771 | ||
|
|
4af0aedebd | ||
|
|
d22464a75e | ||
|
|
82e3a485f0 | ||
|
|
8165ad5879 | ||
|
|
451284de76 | ||
|
|
1d8c7c5e1a | ||
|
|
34be6a3379 |
@@ -23,6 +23,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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,6 +23,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
|
||||
@@ -144,6 +146,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"],
|
||||
|
||||
370
autogpt_platform/backend/backend/server/v2/builder/db.py
Normal 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]
|
||||
87
autogpt_platform/backend/backend/server/v2/builder/model.py
Normal 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
|
||||
227
autogpt_platform/backend/backend/server/v2/builder/routes.py
Normal 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)
|
||||
@@ -448,7 +448,7 @@ async def add_store_agent_to_library(
|
||||
"agentGraphVersion": graph.version,
|
||||
}
|
||||
},
|
||||
include={"AgentGraph": True},
|
||||
include=library_agent_include(user_id),
|
||||
)
|
||||
)
|
||||
if existing_library_agent:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -50,6 +50,7 @@ async def test_get_library_agents_success(
|
||||
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,
|
||||
@@ -66,6 +67,7 @@ async def test_get_library_agents_success(
|
||||
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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -158,7 +158,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,
|
||||
|
||||
@@ -82,9 +82,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.56"
|
||||
|
||||
1063
autogpt_platform/frontend/pnpm-lock.yaml
generated
BIN
autogpt_platform/frontend/public/integrations/anthropic.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
autogpt_platform/frontend/public/integrations/apollo.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/d_id.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
autogpt_platform/frontend/public/integrations/e2b.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/exa.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
autogpt_platform/frontend/public/integrations/fal.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 13 KiB |
BIN
autogpt_platform/frontend/public/integrations/google_maps.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
autogpt_platform/frontend/public/integrations/groq.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
autogpt_platform/frontend/public/integrations/ideogram.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
autogpt_platform/frontend/public/integrations/jina.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
autogpt_platform/frontend/public/integrations/llama_api.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 21 KiB |
BIN
autogpt_platform/frontend/public/integrations/nvidia.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
autogpt_platform/frontend/public/integrations/ollama.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
autogpt_platform/frontend/public/integrations/open_router.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
autogpt_platform/frontend/public/integrations/openai.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
autogpt_platform/frontend/public/integrations/replicate.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
autogpt_platform/frontend/public/integrations/revid.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
autogpt_platform/frontend/public/integrations/screenshotone.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
autogpt_platform/frontend/public/integrations/slant3d.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
autogpt_platform/frontend/public/integrations/smartlead.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 40 KiB |
BIN
autogpt_platform/frontend/public/integrations/twitter.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 38 KiB |
BIN
autogpt_platform/frontend/public/integrations/unreal_speech.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
autogpt_platform/frontend/public/integrations/zerobounce.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
@@ -28,6 +28,7 @@ import "@xyflow/react/dist/style.css";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
import "./flow.css";
|
||||
import {
|
||||
Block,
|
||||
BlockUIType,
|
||||
formatEdgeID,
|
||||
GraphExecutionID,
|
||||
@@ -39,7 +40,6 @@ import { CustomEdge } from "./CustomEdge";
|
||||
import ConnectionLine from "./ConnectionLine";
|
||||
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
||||
import { SaveControl } from "@/components/edit/control/SaveControl";
|
||||
import { BlocksControl } from "@/components/edit/control/BlocksControl";
|
||||
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
|
||||
import { startTutorial } from "./tutorial";
|
||||
import useAgentGraph from "@/hooks/useAgentGraph";
|
||||
@@ -53,6 +53,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
|
||||
@@ -101,7 +102,6 @@ const FlowEditor: React.FC<{
|
||||
setAgentDescription,
|
||||
savedAgent,
|
||||
availableNodes,
|
||||
availableFlows,
|
||||
getOutputType,
|
||||
requestSave,
|
||||
requestSaveAndRun,
|
||||
@@ -136,6 +136,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);
|
||||
@@ -466,13 +470,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.
|
||||
@@ -489,7 +487,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,
|
||||
)
|
||||
@@ -504,19 +502,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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -545,7 +543,6 @@ const FlowEditor: React.FC<{
|
||||
[
|
||||
nodeId,
|
||||
setViewport,
|
||||
availableNodes,
|
||||
addNodes,
|
||||
nodeDimensions,
|
||||
deleteElements,
|
||||
@@ -627,12 +624,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,
|
||||
},
|
||||
];
|
||||
@@ -680,15 +677,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={
|
||||
@@ -701,6 +696,8 @@ const FlowEditor: React.FC<{
|
||||
agentName={agentName}
|
||||
onNameChange={setAgentName}
|
||||
pinSavePopover={pinSavePopover}
|
||||
blockMenuSelected={blockMenuSelected}
|
||||
setBlockMenuSelected={setBlockMenuSelected}
|
||||
/>
|
||||
}
|
||||
></ControlPanel>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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 }>;
|
||||
}
|
||||
|
||||
export 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:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div className="flex flex-1 flex-col items-start gap-0.5">
|
||||
{title && (
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
|
||||
)}
|
||||
>
|
||||
{highlightText(beautifyString(title), highlightedText)}
|
||||
</span>
|
||||
)}
|
||||
{description && (
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
|
||||
)}
|
||||
>
|
||||
{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;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { 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 = (newOpen: boolean) => {
|
||||
if (!pinBlocksPopover) {
|
||||
setOpen(newOpen);
|
||||
setBlockMenuSelected(newOpen ? "block" : "");
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
import React 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";
|
||||
|
||||
export const BlockMenuContent = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
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";
|
||||
import { getDefaultFilters } from "./helpers";
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 500;
|
||||
|
||||
interface BlockMenuSearchBarProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
|
||||
className = "",
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [localQuery, setLocalQuery] = useState("");
|
||||
const { setSearchQuery, searchId, setSearchId, setFilters } =
|
||||
useBlockMenuContext();
|
||||
|
||||
const searchIdRef = useRef(searchId);
|
||||
useEffect(() => {
|
||||
searchIdRef.current = searchId;
|
||||
}, [searchId]);
|
||||
|
||||
const debouncedSetSearchQuery = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value);
|
||||
if (value.length === 0) {
|
||||
setSearchId(undefined);
|
||||
} else if (!searchIdRef.current) {
|
||||
setSearchId(crypto.randomUUID());
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS),
|
||||
[setSearchQuery, setSearchId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
}, [debouncedSetSearchQuery]);
|
||||
|
||||
const handleClear = () => {
|
||||
setLocalQuery("");
|
||||
setSearchQuery("");
|
||||
setSearchId(undefined);
|
||||
setFilters(getDefaultFilters());
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
export 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 && "cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn, parseErrorMessage } 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;
|
||||
}
|
||||
|
||||
export const ErrorState: React.FC<ErrorStateProps> = ({
|
||||
title = "Something went wrong",
|
||||
message,
|
||||
error,
|
||||
onRetry,
|
||||
retryLabel = "Retry",
|
||||
className,
|
||||
showIcon = true,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col items-center justify-center space-y-4 text-center",
|
||||
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">
|
||||
{parseErrorMessage(error, message)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { X } from "lucide-react";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
selected?: boolean;
|
||||
number?: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const FilterChip: React.FC<Props> = ({
|
||||
selected = false,
|
||||
number,
|
||||
name,
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
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:cursor-not-allowed",
|
||||
selected && "border-0 bg-violet-700 hover:border",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<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 && (
|
||||
<>
|
||||
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-zinc-50 transition-all duration-300 ease-in-out group-hover:hidden">
|
||||
<X
|
||||
className="h-3 w-3 rounded-full text-violet-700"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</span>
|
||||
{number !== undefined && (
|
||||
<span className="hidden 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 group-hover:flex">
|
||||
{number > 100 ? "100+" : number}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
export 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">
|
||||
{title && (
|
||||
<p className="line-clamp-1 flex-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-700 group-disabled:text-zinc-400">
|
||||
{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;
|
||||
@@ -0,0 +1,119 @@
|
||||
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;
|
||||
|
||||
function escapeRegExp(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const escaped = escapeRegExp(highlight);
|
||||
const parts = text.split(new RegExp(`(${escaped})`, "gi"));
|
||||
return parts.map((part, i) =>
|
||||
part.toLowerCase() === highlight?.toLowerCase() ? (
|
||||
<mark key={i} className="bg-transparent font-bold">
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export 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:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
{...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">
|
||||
{title && (
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-1 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800 group-disabled:text-zinc-400",
|
||||
)}
|
||||
>
|
||||
{highlightText(beautifyString(title), highlightedText)}
|
||||
</span>
|
||||
)}
|
||||
{description && (
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
|
||||
)}
|
||||
>
|
||||
{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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
export 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>
|
||||
{name && (
|
||||
<span className="truncate font-sans text-sm font-normal leading-[1.375rem] text-zinc-800">
|
||||
{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;
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
export 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">
|
||||
{title && (
|
||||
<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;
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
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 }>;
|
||||
}
|
||||
|
||||
export 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;
|
||||
@@ -0,0 +1,117 @@
|
||||
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 }>;
|
||||
}
|
||||
|
||||
export 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:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{image_url && (
|
||||
<div className="relative h-[3.125rem] w-[5.625rem] overflow-hidden rounded-[0.375rem] bg-white">
|
||||
<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">
|
||||
{title && (
|
||||
<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">
|
||||
{edited_time && (
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-1 font-sans text-xs font-normal leading-5 text-zinc-500 group-disabled:text-zinc-400",
|
||||
)}
|
||||
>
|
||||
Edited {<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;
|
||||
@@ -0,0 +1,176 @@
|
||||
"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";
|
||||
import { getDefaultFilters } from "./helpers";
|
||||
|
||||
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>(getDefaultFilters());
|
||||
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(
|
||||
"useBlockMenuContext must be used within a BlockMenuStateProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
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";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const AllBlocksContent = () => {
|
||||
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();
|
||||
}, [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={scrollbarStyles}>
|
||||
<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={scrollbarStyles}>
|
||||
<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) => (
|
||||
<Block
|
||||
key={`${category.name}-${block.id}`}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { BlockMenuSidebar } from "./BlockMenuSidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { BlockMenuDefaultContent } from "./BlockMenuDefaultContent";
|
||||
|
||||
export const BlockMenuDefault = () => {
|
||||
return (
|
||||
<div className="flex flex-1 overflow-y-auto">
|
||||
<BlockMenuSidebar />
|
||||
<Separator className="h-full w-[1px] text-zinc-300" />
|
||||
<BlockMenuDefaultContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
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 { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { PaginatedBlocksContent } from "./PaginatedBlocksContent";
|
||||
|
||||
export interface ActionBlock {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface BlockListType {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const BlockMenuDefaultContent = () => {
|
||||
const { defaultState } = useBlockMenuContext();
|
||||
|
||||
return (
|
||||
<div className="h-full flex-1 overflow-hidden">
|
||||
{defaultState == "suggestion" && <SuggestionContent />}
|
||||
{defaultState == "all_blocks" && <AllBlocksContent />}
|
||||
{defaultState == "input_blocks" && (
|
||||
<PaginatedBlocksContent blockRequest={{ type: "input" }} />
|
||||
)}
|
||||
{defaultState == "action_blocks" && (
|
||||
<PaginatedBlocksContent blockRequest={{ type: "action" }} />
|
||||
)}
|
||||
{defaultState == "output_blocks" && (
|
||||
<PaginatedBlocksContent blockRequest={{ type: "output" }} />
|
||||
)}
|
||||
{defaultState == "integrations" && <IntegrationsContent />}
|
||||
{defaultState == "marketplace_agents" && <MarketplaceAgentsContent />}
|
||||
{defaultState == "my_agents" && <MyAgentsContent />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
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";
|
||||
|
||||
export const BlockMenuSidebar = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
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";
|
||||
|
||||
export const IntegrationBlocks = () => {
|
||||
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();
|
||||
}, [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) => (
|
||||
<IntegrationBlock
|
||||
key={block.id}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
icon_url={`/integrations/${integration}.png`}
|
||||
onClick={() => {
|
||||
addNode(block);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { PaginatedIntegrationList } from "./PaginatedIntegrationList";
|
||||
import { IntegrationBlocks } from "./IntegrationBlocks";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const IntegrationsContent = () => {
|
||||
const { integration } = useBlockMenuContext();
|
||||
|
||||
if (!integration) {
|
||||
return <PaginatedIntegrationList />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={scrollbarStyles}>
|
||||
<div className="w-full px-4 pb-4">
|
||||
<IntegrationBlocks />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import { MarketplaceAgentBlock } from "../MarketplaceAgentBlock";
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
import { ErrorState } from "../ErrorState";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const MarketplaceAgentsContent = () => {
|
||||
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={scrollbarStyles}>
|
||||
<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={scrollbarStyles}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
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";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const MyAgentsContent = () => {
|
||||
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={scrollbarStyles}>
|
||||
<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={scrollbarStyles}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React 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";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
interface PaginatedBlocksContentProps {
|
||||
blockRequest: BlockRequest;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export 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={scrollbarStyles}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { Integration } from "../Integration";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
import { ErrorState } from "../ErrorState";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const PaginatedIntegrationList = () => {
|
||||
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={scrollbarStyles}>
|
||||
<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={scrollbarStyles}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
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";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const SuggestionContent = () => {
|
||||
const { setIntegration, setDefaultState, 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();
|
||||
}, [fetchSuggestions]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full p-4">
|
||||
<ErrorState
|
||||
title="Failed to load suggestions"
|
||||
error={error}
|
||||
onRetry={fetchSuggestions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={scrollbarStyles}>
|
||||
<div className="w-full space-y-6 pb-4">
|
||||
{/* Integrations */}
|
||||
<div className="space-y-2.5 px-4">
|
||||
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
|
||||
Integrations
|
||||
</p>
|
||||
<div className="grid grid-cols-3 grid-rows-2 gap-2">
|
||||
{!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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Filters } from "./block-menu-provider";
|
||||
|
||||
export const getDefaultFilters = (): Filters => ({
|
||||
categories: {
|
||||
blocks: false,
|
||||
integrations: false,
|
||||
marketplace_agents: false,
|
||||
my_agents: false,
|
||||
providers: false,
|
||||
},
|
||||
createdBy: [],
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export 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",
|
||||
className,
|
||||
)}
|
||||
{...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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
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";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
export const BlockMenuSearch = () => {
|
||||
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={cn(scrollbarStyles, "space-y-4 py-4")}>
|
||||
{searchData.length !== 0 && <FiltersList />}
|
||||
<SearchList
|
||||
isLoading={isLoading}
|
||||
loadingMore={loadingMore}
|
||||
hasMore={hasMore}
|
||||
error={error}
|
||||
onRetry={() => {
|
||||
setPage(1);
|
||||
setError(null);
|
||||
fetchSearchData(1, false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,255 @@
|
||||
import { FilterChip } from "../FilterChip";
|
||||
import { useState, useEffect } 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";
|
||||
import { getDefaultFilters } from "../helpers";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbar";
|
||||
|
||||
const INITIAL_CREATORS_TO_SHOW = 5;
|
||||
|
||||
export 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[]>([]);
|
||||
const [displayedCreatorsCount, setDisplayedCreatorsCount] = useState(
|
||||
INITIAL_CREATORS_TO_SHOW,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsSheetVisible(true);
|
||||
setLocalFilters(filters);
|
||||
setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW); // Reset on open
|
||||
|
||||
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 = (category: CategoryKey) => {
|
||||
setLocalFilters((prev) => ({
|
||||
...prev,
|
||||
categories: {
|
||||
...prev.categories,
|
||||
[category]: !prev.categories[category],
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const onCreatorChange = (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 = () => {
|
||||
setFilters(localFilters);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
const clearedFilters: Filters = getDefaultFilters();
|
||||
setFilters(clearedFilters);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const hasLocalActiveFilters = () => {
|
||||
const hasCategoryFilter = Object.values(localFilters.categories).some(
|
||||
(value) => value,
|
||||
);
|
||||
const hasCreatorFilter = localFilters.createdBy.length > 0;
|
||||
|
||||
return hasCategoryFilter || hasCreatorFilter;
|
||||
};
|
||||
|
||||
const hasActiveFilters = () => {
|
||||
const hasCategoryFilter = Object.values(filters.categories).some(
|
||||
(value) => value,
|
||||
);
|
||||
const hasCreatorFilter = filters.createdBy.length > 0;
|
||||
|
||||
return hasCategoryFilter || hasCreatorFilter;
|
||||
};
|
||||
|
||||
const handleToggleShowMoreCreators = () => {
|
||||
if (displayedCreatorsCount < creators.length) {
|
||||
setDisplayedCreatorsCount(creators.length);
|
||||
} else {
|
||||
setDisplayedCreatorsCount(INITIAL_CREATORS_TO_SHOW);
|
||||
}
|
||||
};
|
||||
|
||||
const visibleCreators = creators.slice(0, displayedCreatorsCount);
|
||||
|
||||
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", scrollbarStyles)}>
|
||||
{/* 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">
|
||||
{visibleCreators.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 > INITIAL_CREATORS_TO_SHOW && (
|
||||
<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"
|
||||
onClick={handleToggleShowMoreCreators}
|
||||
>
|
||||
{displayedCreatorsCount < creators.length ? "More" : "Less"}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from "react";
|
||||
import { FilterChip } from "../FilterChip";
|
||||
import { FilterSheet } from "./FilterSheet";
|
||||
import { CategoryKey, useBlockMenuContext } from "../block-menu-provider";
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Frown } from "lucide-react";
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -10,12 +10,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { IconSave } from "@/components/ui/icons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
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 +22,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 +49,8 @@ export const SaveControl = ({
|
||||
onNameChange,
|
||||
agentDescription,
|
||||
onDescriptionChange,
|
||||
blockMenuSelected,
|
||||
setBlockMenuSelected,
|
||||
pinSavePopover,
|
||||
}: SaveControlProps) => {
|
||||
/**
|
||||
@@ -82,27 +85,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">
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const scrollbarStyles =
|
||||
"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";
|
||||
@@ -13,7 +13,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 +21,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>
|
||||
));
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
1
autogpt_platform/frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { usePagination } from "./usePagination";
|
||||
@@ -14,6 +14,7 @@ import BackendAPI, {
|
||||
GraphMeta,
|
||||
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,
|
||||
@@ -281,15 +288,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;
|
||||
};
|
||||
@@ -1120,7 +1129,6 @@ export default function useAgentGraph(
|
||||
setAgentDescription,
|
||||
savedAgent,
|
||||
availableNodes,
|
||||
availableFlows,
|
||||
getOutputType,
|
||||
requestSave,
|
||||
requestSaveAndRun,
|
||||
|
||||
232
autogpt_platform/frontend/src/hooks/usePagination.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { useState, useCallback, useRef, useEffect } 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,
|
||||
};
|
||||
};
|
||||
@@ -9,6 +9,11 @@ import type {
|
||||
APIKeyCredentials,
|
||||
APIKeyPermission,
|
||||
Block,
|
||||
BlockCategoryResponse,
|
||||
BlockRequest,
|
||||
BlockResponse,
|
||||
BlockSearchResponse,
|
||||
CountResponse,
|
||||
CreateAPIKeyResponse,
|
||||
CreatorDetails,
|
||||
CreatorsResponse,
|
||||
@@ -42,6 +47,7 @@ import type {
|
||||
OttoQuery,
|
||||
OttoResponse,
|
||||
ProfileDetails,
|
||||
ProviderResponse,
|
||||
RefundRequest,
|
||||
ReviewSubmissionRequest,
|
||||
Schedule,
|
||||
@@ -56,6 +62,7 @@ import type {
|
||||
StoreSubmissionRequest,
|
||||
StoreSubmissionsResponse,
|
||||
SubmissionStatus,
|
||||
SuggestionsResponse,
|
||||
TransactionHistory,
|
||||
User,
|
||||
UserOnboarding,
|
||||
@@ -206,6 +213,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 ////////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
@@ -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;
|
||||
@@ -402,6 +467,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;
|
||||
|
||||