Compare commits
127 Commits
testing-cl
...
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")
|
user_id: str = SchemaField(description="User ID")
|
||||||
graph_id: str = SchemaField(description="Graph ID")
|
graph_id: str = SchemaField(description="Graph ID")
|
||||||
graph_version: int = SchemaField(description="Graph Version")
|
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")
|
inputs: BlockInput = SchemaField(description="Input data for the graph")
|
||||||
input_schema: dict = SchemaField(description="Input schema 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]
|
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):
|
class RequestTopUp(pydantic.BaseModel):
|
||||||
credit_amount: int
|
credit_amount: int
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import backend.server.routers.postmark.postmark
|
|||||||
import backend.server.routers.v1
|
import backend.server.routers.v1
|
||||||
import backend.server.v2.admin.credit_admin_routes
|
import backend.server.v2.admin.credit_admin_routes
|
||||||
import backend.server.v2.admin.store_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.db
|
||||||
import backend.server.v2.library.model
|
import backend.server.v2.library.model
|
||||||
import backend.server.v2.library.routes
|
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(
|
app.include_router(
|
||||||
backend.server.v2.store.routes.router, tags=["v2"], prefix="/api/store"
|
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(
|
app.include_router(
|
||||||
backend.server.v2.admin.store_admin_routes.router,
|
backend.server.v2.admin.store_admin_routes.router,
|
||||||
tags=["v2", "admin"],
|
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,
|
"agentGraphVersion": graph.version,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
include={"AgentGraph": True},
|
include=library_agent_include(user_id),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if existing_library_agent:
|
if existing_library_agent:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
|
|
||||||
# Made input_schema and output_schema match GraphMeta's type
|
# Made input_schema and output_schema match GraphMeta's type
|
||||||
input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
|
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)
|
# Indicates whether there's a new output (based on recent runs)
|
||||||
new_output: bool
|
new_output: bool
|
||||||
@@ -106,6 +107,7 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
name=graph.name,
|
name=graph.name,
|
||||||
description=graph.description,
|
description=graph.description,
|
||||||
input_schema=graph.input_schema,
|
input_schema=graph.input_schema,
|
||||||
|
output_schema=graph.output_schema,
|
||||||
new_output=new_output,
|
new_output=new_output,
|
||||||
can_access_graph=can_access_graph,
|
can_access_graph=can_access_graph,
|
||||||
is_latest_version=is_latest_version,
|
is_latest_version=is_latest_version,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ async def test_get_library_agents_success(
|
|||||||
creator_name="Test Creator",
|
creator_name="Test Creator",
|
||||||
creator_image_url="",
|
creator_image_url="",
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
|
output_schema={"type": "object", "properties": {}},
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
can_access_graph=True,
|
can_access_graph=True,
|
||||||
@@ -66,6 +67,7 @@ async def test_get_library_agents_success(
|
|||||||
creator_name="Test Creator",
|
creator_name="Test Creator",
|
||||||
creator_image_url="",
|
creator_image_url="",
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
|
output_schema={"type": "object", "properties": {}},
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
can_access_graph=False,
|
can_access_graph=False,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def sanitize_query(query: str | None) -> str | None:
|
|||||||
|
|
||||||
async def get_store_agents(
|
async def get_store_agents(
|
||||||
featured: bool = False,
|
featured: bool = False,
|
||||||
creator: str | None = None,
|
creators: list[str] | None = None,
|
||||||
sorted_by: str | None = None,
|
sorted_by: str | None = None,
|
||||||
search_query: str | None = None,
|
search_query: str | None = None,
|
||||||
category: str | None = None,
|
category: str | None = None,
|
||||||
@@ -48,15 +48,15 @@ async def get_store_agents(
|
|||||||
Get PUBLIC store agents from the StoreAgent view
|
Get PUBLIC store agents from the StoreAgent view
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
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)
|
sanitized_query = sanitize_query(search_query)
|
||||||
|
|
||||||
where_clause = {}
|
where_clause = {}
|
||||||
if featured:
|
if featured:
|
||||||
where_clause["featured"] = featured
|
where_clause["featured"] = featured
|
||||||
if creator:
|
if creators:
|
||||||
where_clause["creator_username"] = creator
|
where_clause["creator_username"] = {"in": creators}
|
||||||
if category:
|
if category:
|
||||||
where_clause["categories"] = {"has": category}
|
where_clause["categories"] = {"has": category}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ async def get_agents(
|
|||||||
try:
|
try:
|
||||||
agents = await backend.server.v2.store.db.get_store_agents(
|
agents = await backend.server.v2.store.db.get_store_agents(
|
||||||
featured=featured,
|
featured=featured,
|
||||||
creator=creator,
|
creators=[creator] if creator else None,
|
||||||
sorted_by=sorted_by,
|
sorted_by=sorted_by,
|
||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
category=category,
|
category=category,
|
||||||
|
|||||||
@@ -82,9 +82,12 @@
|
|||||||
"react-markdown": "9.0.3",
|
"react-markdown": "9.0.3",
|
||||||
"react-modal": "3.16.3",
|
"react-modal": "3.16.3",
|
||||||
"react-shepherd": "6.1.8",
|
"react-shepherd": "6.1.8",
|
||||||
|
"react-timeago": "^8.2.0",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"shepherd.js": "14.5.0",
|
"shepherd.js": "14.5.0",
|
||||||
"tailwind-merge": "2.6.0",
|
"tailwind-merge": "2.6.0",
|
||||||
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
|
"tailwind-scrollbar-hide": "^2.0.0",
|
||||||
"tailwindcss-animate": "1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"zod": "3.25.56"
|
"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 { CustomNode } from "./CustomNode";
|
||||||
import "./flow.css";
|
import "./flow.css";
|
||||||
import {
|
import {
|
||||||
|
Block,
|
||||||
BlockUIType,
|
BlockUIType,
|
||||||
formatEdgeID,
|
formatEdgeID,
|
||||||
GraphExecutionID,
|
GraphExecutionID,
|
||||||
@@ -39,7 +40,6 @@ import { CustomEdge } from "./CustomEdge";
|
|||||||
import ConnectionLine from "./ConnectionLine";
|
import ConnectionLine from "./ConnectionLine";
|
||||||
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
||||||
import { SaveControl } from "@/components/edit/control/SaveControl";
|
import { SaveControl } from "@/components/edit/control/SaveControl";
|
||||||
import { BlocksControl } from "@/components/edit/control/BlocksControl";
|
|
||||||
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
|
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
|
||||||
import { startTutorial } from "./tutorial";
|
import { startTutorial } from "./tutorial";
|
||||||
import useAgentGraph from "@/hooks/useAgentGraph";
|
import useAgentGraph from "@/hooks/useAgentGraph";
|
||||||
@@ -53,6 +53,7 @@ import OttoChatWidget from "@/components/OttoChatWidget";
|
|||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||||
import { CronScheduler } from "./cronScheduler";
|
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
|
// 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
|
// 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,
|
setAgentDescription,
|
||||||
savedAgent,
|
savedAgent,
|
||||||
availableNodes,
|
availableNodes,
|
||||||
availableFlows,
|
|
||||||
getOutputType,
|
getOutputType,
|
||||||
requestSave,
|
requestSave,
|
||||||
requestSaveAndRun,
|
requestSaveAndRun,
|
||||||
@@ -136,6 +136,10 @@ const FlowEditor: React.FC<{
|
|||||||
// State to control if save popover should be pinned open
|
// State to control if save popover should be pinned open
|
||||||
const [pinSavePopover, setPinSavePopover] = useState(false);
|
const [pinSavePopover, setPinSavePopover] = useState(false);
|
||||||
|
|
||||||
|
const [blockMenuSelected, setBlockMenuSelected] = useState<
|
||||||
|
"save" | "block" | ""
|
||||||
|
>("");
|
||||||
|
|
||||||
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
|
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
|
||||||
|
|
||||||
const [openCron, setOpenCron] = useState(false);
|
const [openCron, setOpenCron] = useState(false);
|
||||||
@@ -466,13 +470,7 @@ const FlowEditor: React.FC<{
|
|||||||
}, [nodes, setViewport, x, y]);
|
}, [nodes, setViewport, x, y]);
|
||||||
|
|
||||||
const addNode = useCallback(
|
const addNode = useCallback(
|
||||||
(blockId: string, nodeType: string, hardcodedValues: any = {}) => {
|
(block: Block) => {
|
||||||
const nodeSchema = availableNodes.find((node) => node.id === blockId);
|
|
||||||
if (!nodeSchema) {
|
|
||||||
console.error(`Schema not found for block ID: ${blockId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Calculate a position to the right of the newly added block, allowing for some margin.
|
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.
|
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
|
? // we will get all the dimension of nodes, then store
|
||||||
findNewlyAddedBlockCoordinates(
|
findNewlyAddedBlockCoordinates(
|
||||||
nodeDimensions,
|
nodeDimensions,
|
||||||
nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500,
|
block.uiType == BlockUIType.NOTE ? 300 : 500,
|
||||||
60,
|
60,
|
||||||
1.0,
|
1.0,
|
||||||
)
|
)
|
||||||
@@ -504,19 +502,19 @@ const FlowEditor: React.FC<{
|
|||||||
type: "custom",
|
type: "custom",
|
||||||
position: viewportCoordinates, // Set the position to the calculated viewport center
|
position: viewportCoordinates, // Set the position to the calculated viewport center
|
||||||
data: {
|
data: {
|
||||||
blockType: nodeType,
|
blockType: block.name,
|
||||||
blockCosts: nodeSchema.costs,
|
blockCosts: block.costs,
|
||||||
title: `${nodeType} ${nodeId}`,
|
title: `${block.name} ${nodeId}`,
|
||||||
description: nodeSchema.description,
|
description: block.description,
|
||||||
categories: nodeSchema.categories,
|
categories: block.categories,
|
||||||
inputSchema: nodeSchema.inputSchema,
|
inputSchema: block.inputSchema,
|
||||||
outputSchema: nodeSchema.outputSchema,
|
outputSchema: block.outputSchema,
|
||||||
hardcodedValues: hardcodedValues,
|
hardcodedValues: block.hardcodedValues || {},
|
||||||
connections: [],
|
connections: [],
|
||||||
isOutputOpen: false,
|
isOutputOpen: false,
|
||||||
block_id: blockId,
|
block_id: block.id,
|
||||||
isOutputStatic: nodeSchema.staticOutput,
|
isOutputStatic: block.staticOutput,
|
||||||
uiType: nodeSchema.uiType,
|
uiType: block.uiType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -545,7 +543,6 @@ const FlowEditor: React.FC<{
|
|||||||
[
|
[
|
||||||
nodeId,
|
nodeId,
|
||||||
setViewport,
|
setViewport,
|
||||||
availableNodes,
|
|
||||||
addNodes,
|
addNodes,
|
||||||
nodeDimensions,
|
nodeDimensions,
|
||||||
deleteElements,
|
deleteElements,
|
||||||
@@ -627,12 +624,12 @@ const FlowEditor: React.FC<{
|
|||||||
const editorControls: Control[] = [
|
const editorControls: Control[] = [
|
||||||
{
|
{
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
icon: <IconUndo2 />,
|
icon: <IconUndo2 className="h-5 w-5" strokeWidth={2} />,
|
||||||
onClick: handleUndo,
|
onClick: handleUndo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Redo",
|
label: "Redo",
|
||||||
icon: <IconRedo2 />,
|
icon: <IconRedo2 className="h-5 w-5" strokeWidth={2} />,
|
||||||
onClick: handleRedo,
|
onClick: handleRedo,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -680,15 +677,13 @@ const FlowEditor: React.FC<{
|
|||||||
<Controls />
|
<Controls />
|
||||||
<Background className="dark:bg-slate-800" />
|
<Background className="dark:bg-slate-800" />
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
className="absolute z-20"
|
|
||||||
controls={editorControls}
|
controls={editorControls}
|
||||||
topChildren={
|
topChildren={
|
||||||
<BlocksControl
|
<BlockMenu
|
||||||
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
|
pinBlocksPopover={pinBlocksPopover}
|
||||||
blocks={availableNodes}
|
addNode={addNode}
|
||||||
addBlock={addNode}
|
blockMenuSelected={blockMenuSelected}
|
||||||
flows={availableFlows}
|
setBlockMenuSelected={setBlockMenuSelected}
|
||||||
nodes={nodes}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
botChildren={
|
botChildren={
|
||||||
@@ -701,6 +696,8 @@ const FlowEditor: React.FC<{
|
|||||||
agentName={agentName}
|
agentName={agentName}
|
||||||
onNameChange={setAgentName}
|
onNameChange={setAgentName}
|
||||||
pinSavePopover={pinSavePopover}
|
pinSavePopover={pinSavePopover}
|
||||||
|
blockMenuSelected={blockMenuSelected}
|
||||||
|
setBlockMenuSelected={setBlockMenuSelected}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
></ControlPanel>
|
></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 { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { ControlPanelButton } from "@/components/builder/block-menu/ControlPanelButton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a control element for the ControlPanel Component.
|
* Represents a control element for the ControlPanel Component.
|
||||||
@@ -27,6 +21,7 @@ interface ControlPanelProps {
|
|||||||
controls: Control[];
|
controls: Control[];
|
||||||
topChildren?: React.ReactNode;
|
topChildren?: React.ReactNode;
|
||||||
botChildren?: React.ReactNode;
|
botChildren?: React.ReactNode;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,42 +40,31 @@ export const ControlPanel = ({
|
|||||||
className,
|
className,
|
||||||
}: ControlPanelProps) => {
|
}: ControlPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<Card className={cn("m-4 mt-24 w-14 dark:bg-slate-900", className)}>
|
<section
|
||||||
<CardContent className="p-0">
|
className={cn(
|
||||||
<div className="flex flex-col items-center gap-3 rounded-xl py-3">
|
"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)]",
|
||||||
{topChildren}
|
className,
|
||||||
<Separator className="dark:bg-slate-700" />
|
)}
|
||||||
{controls.map((control, index) => (
|
>
|
||||||
<Tooltip key={index} delayDuration={500}>
|
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
|
||||||
<TooltipTrigger asChild>
|
{topChildren}
|
||||||
<div>
|
<Separator className="text-[#E1E1E1]" />
|
||||||
<Button
|
{controls.map((control, index) => (
|
||||||
variant="ghost"
|
<ControlPanelButton
|
||||||
size="icon"
|
key={index}
|
||||||
onClick={() => control.onClick()}
|
onClick={() => control.onClick()}
|
||||||
data-id={`control-button-${index}`}
|
data-id={`control-button-${index}`}
|
||||||
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
|
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
|
||||||
disabled={control.disabled || false}
|
disabled={control.disabled || false}
|
||||||
className="dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
|
className="rounded-none"
|
||||||
>
|
>
|
||||||
{control.icon}
|
{control.icon}
|
||||||
<span className="sr-only">{control.label}</span>
|
</ControlPanelButton>
|
||||||
</Button>
|
))}
|
||||||
</div>
|
<Separator className="text-[#E1E1E1]" />
|
||||||
</TooltipTrigger>
|
{botChildren}
|
||||||
<TooltipContent
|
</div>
|
||||||
side="right"
|
</section>
|
||||||
className="dark:bg-slate-800 dark:text-slate-100"
|
|
||||||
>
|
|
||||||
{control.label}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
<Separator className="dark:bg-slate-700" />
|
|
||||||
{botChildren}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default ControlPanel;
|
export default ControlPanel;
|
||||||
|
|||||||
@@ -10,12 +10,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { IconSave } from "@/components/ui/icons";
|
import { IconSave } from "@/components/ui/icons";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { ControlPanelButton } from "@/components/builder/block-menu/ControlPanelButton";
|
||||||
|
|
||||||
interface SaveControlProps {
|
interface SaveControlProps {
|
||||||
agentMeta: GraphMeta | null;
|
agentMeta: GraphMeta | null;
|
||||||
@@ -26,6 +22,11 @@ interface SaveControlProps {
|
|||||||
onNameChange: (name: string) => void;
|
onNameChange: (name: string) => void;
|
||||||
onDescriptionChange: (description: string) => void;
|
onDescriptionChange: (description: string) => void;
|
||||||
pinSavePopover: boolean;
|
pinSavePopover: boolean;
|
||||||
|
|
||||||
|
blockMenuSelected: "save" | "block" | "";
|
||||||
|
setBlockMenuSelected: React.Dispatch<
|
||||||
|
React.SetStateAction<"" | "save" | "block">
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +49,8 @@ export const SaveControl = ({
|
|||||||
onNameChange,
|
onNameChange,
|
||||||
agentDescription,
|
agentDescription,
|
||||||
onDescriptionChange,
|
onDescriptionChange,
|
||||||
|
blockMenuSelected,
|
||||||
|
setBlockMenuSelected,
|
||||||
pinSavePopover,
|
pinSavePopover,
|
||||||
}: SaveControlProps) => {
|
}: SaveControlProps) => {
|
||||||
/**
|
/**
|
||||||
@@ -82,27 +85,29 @@ export const SaveControl = ({
|
|||||||
}, [handleSave, toast]);
|
}, [handleSave, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={pinSavePopover ? true : undefined}>
|
<Popover
|
||||||
<Tooltip delayDuration={500}>
|
open={pinSavePopover ? true : undefined}
|
||||||
<TooltipTrigger asChild>
|
onOpenChange={(open) => open || setBlockMenuSelected("")}
|
||||||
<PopoverTrigger asChild>
|
>
|
||||||
<Button
|
<PopoverTrigger>
|
||||||
variant="ghost"
|
<ControlPanelButton
|
||||||
size="icon"
|
data-id="save-control-popover-trigger"
|
||||||
data-id="save-control-popover-trigger"
|
data-testid="blocks-control-save-button"
|
||||||
data-testid="blocks-control-save-button"
|
selected={blockMenuSelected === "save"}
|
||||||
name="Save"
|
onClick={() => {
|
||||||
>
|
setBlockMenuSelected("save");
|
||||||
<IconSave className="dark:text-gray-300" />
|
}}
|
||||||
</Button>
|
className="rounded-none"
|
||||||
</PopoverTrigger>
|
>
|
||||||
</TooltipTrigger>
|
<IconSave className="h-5 w-5" strokeWidth={2} />
|
||||||
<TooltipContent side="right">Save</TooltipContent>
|
</ControlPanelButton>
|
||||||
</Tooltip>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
side="right"
|
side="right"
|
||||||
sideOffset={15}
|
sideOffset={16}
|
||||||
align="start"
|
align="start"
|
||||||
|
className="w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
|
||||||
data-id="save-control-popover-content"
|
data-id="save-control-popover-content"
|
||||||
>
|
>
|
||||||
<Card className="border-none shadow-none dark:bg-slate-900">
|
<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
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -21,7 +21,7 @@ const Checkbox = React.forwardRef<
|
|||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={cn("flex items-center justify-center text-current")}
|
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.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ const MultiSelectorList = forwardRef<
|
|||||||
<CommandList
|
<CommandList
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
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,
|
GraphMeta,
|
||||||
NodeExecutionResult,
|
NodeExecutionResult,
|
||||||
SpecialBlockID,
|
SpecialBlockID,
|
||||||
|
Node,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
import {
|
import {
|
||||||
deepEquals,
|
deepEquals,
|
||||||
@@ -177,6 +178,16 @@ export default function useAgentGraph(
|
|||||||
setAgentName(graph.name);
|
setAgentName(graph.name);
|
||||||
setAgentDescription(graph.description);
|
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) => {
|
setNodes((prevNodes) => {
|
||||||
const _newNodes = graph.nodes.map((node) => {
|
const _newNodes = graph.nodes.map((node) => {
|
||||||
const block = availableNodes.find(
|
const block = availableNodes.find(
|
||||||
@@ -184,12 +195,8 @@ export default function useAgentGraph(
|
|||||||
)!;
|
)!;
|
||||||
if (!block) return null;
|
if (!block) return null;
|
||||||
const prevNode = prevNodes.find((n) => n.id === node.id);
|
const prevNode = prevNodes.find((n) => n.id === node.id);
|
||||||
const flow =
|
const graphName =
|
||||||
block.uiType == BlockUIType.AGENT
|
(block.uiType == BlockUIType.AGENT && getGraphName(node)) || null;
|
||||||
? availableFlows.find(
|
|
||||||
(flow) => flow.id === node.input_default.graph_id,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const newNode: CustomNode = {
|
const newNode: CustomNode = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: "custom",
|
type: "custom",
|
||||||
@@ -201,7 +208,7 @@ export default function useAgentGraph(
|
|||||||
isOutputOpen: false,
|
isOutputOpen: false,
|
||||||
...prevNode?.data,
|
...prevNode?.data,
|
||||||
block_id: block.id,
|
block_id: block.id,
|
||||||
blockType: flow?.name || block.name,
|
blockType: graphName || block.name,
|
||||||
blockCosts: block.costs,
|
blockCosts: block.costs,
|
||||||
categories: block.categories,
|
categories: block.categories,
|
||||||
description: block.description,
|
description: block.description,
|
||||||
@@ -281,15 +288,17 @@ export default function useAgentGraph(
|
|||||||
|
|
||||||
const getToolFuncName = (nodeId: string) => {
|
const getToolFuncName = (nodeId: string) => {
|
||||||
const sinkNode = nodes.find((node) => node.id === nodeId);
|
const sinkNode = nodes.find((node) => node.id === nodeId);
|
||||||
const sinkNodeName = sinkNode
|
|
||||||
? sinkNode.data.block_id === SpecialBlockID.AGENT
|
if (!sinkNode) return "";
|
||||||
? sinkNode.data.hardcodedValues?.graph_id
|
|
||||||
? availableFlows.find(
|
const sinkNodeName =
|
||||||
(flow) => flow.id === sinkNode.data.hardcodedValues.graph_id,
|
sinkNode.data.block_id === SpecialBlockID.AGENT
|
||||||
)?.name || "agentexecutorblock"
|
? sinkNode.data.hardcodedValues?.agent_name ||
|
||||||
: "agentexecutorblock"
|
availableFlows.find(
|
||||||
: sinkNode.data.title.split(" ")[0]
|
(flow) => flow.id === sinkNode.data.hardcodedValues.graph_id,
|
||||||
: "";
|
)?.name ||
|
||||||
|
"agentexecutorblock"
|
||||||
|
: sinkNode.data.title.split(" ")[0];
|
||||||
|
|
||||||
return sinkNodeName;
|
return sinkNodeName;
|
||||||
};
|
};
|
||||||
@@ -1120,7 +1129,6 @@ export default function useAgentGraph(
|
|||||||
setAgentDescription,
|
setAgentDescription,
|
||||||
savedAgent,
|
savedAgent,
|
||||||
availableNodes,
|
availableNodes,
|
||||||
availableFlows,
|
|
||||||
getOutputType,
|
getOutputType,
|
||||||
requestSave,
|
requestSave,
|
||||||
requestSaveAndRun,
|
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,
|
APIKeyCredentials,
|
||||||
APIKeyPermission,
|
APIKeyPermission,
|
||||||
Block,
|
Block,
|
||||||
|
BlockCategoryResponse,
|
||||||
|
BlockRequest,
|
||||||
|
BlockResponse,
|
||||||
|
BlockSearchResponse,
|
||||||
|
CountResponse,
|
||||||
CreateAPIKeyResponse,
|
CreateAPIKeyResponse,
|
||||||
CreatorDetails,
|
CreatorDetails,
|
||||||
CreatorsResponse,
|
CreatorsResponse,
|
||||||
@@ -42,6 +47,7 @@ import type {
|
|||||||
OttoQuery,
|
OttoQuery,
|
||||||
OttoResponse,
|
OttoResponse,
|
||||||
ProfileDetails,
|
ProfileDetails,
|
||||||
|
ProviderResponse,
|
||||||
RefundRequest,
|
RefundRequest,
|
||||||
ReviewSubmissionRequest,
|
ReviewSubmissionRequest,
|
||||||
Schedule,
|
Schedule,
|
||||||
@@ -56,6 +62,7 @@ import type {
|
|||||||
StoreSubmissionRequest,
|
StoreSubmissionRequest,
|
||||||
StoreSubmissionsResponse,
|
StoreSubmissionsResponse,
|
||||||
SubmissionStatus,
|
SubmissionStatus,
|
||||||
|
SuggestionsResponse,
|
||||||
TransactionHistory,
|
TransactionHistory,
|
||||||
User,
|
User,
|
||||||
UserOnboarding,
|
UserOnboarding,
|
||||||
@@ -206,6 +213,44 @@ export default class BackendAPI {
|
|||||||
return this._get("/onboarding/enabled");
|
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 ////////////////
|
//////////////// GRAPHS ////////////////
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
|
|||||||
@@ -27,6 +27,71 @@ export type BlockCost = {
|
|||||||
cost_filter: { [key: string]: any };
|
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 */
|
/* Mirror of backend/data/block.py:Block */
|
||||||
export type Block = {
|
export type Block = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -402,6 +467,7 @@ export type LibraryAgent = {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
input_schema: BlockIOObjectSubSchema;
|
input_schema: BlockIOObjectSubSchema;
|
||||||
|
output_schema: BlockIOObjectSubSchema;
|
||||||
new_output: boolean;
|
new_output: boolean;
|
||||||
can_access_graph: boolean;
|
can_access_graph: boolean;
|
||||||
is_latest_version: boolean;
|
is_latest_version: boolean;
|
||||||
|
|||||||