mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(platform/builder): Builder credentials support + UX improvements (#10323)
- Resolves #10313 - Resolves #10333 Before: https://github.com/user-attachments/assets/a105b2b0-a90b-4bc6-89da-bef3f5a5fa1f - No credentials input - Stuttery experience when panning or zooming the viewport After: https://github.com/user-attachments/assets/f58d7864-055f-4e1c-a221-57154467c3aa - Pretty much the same UX as in the Library, with fully-fledged credentials input support - Much smoother when moving around the canvas ### Changes 🏗️ Frontend: - Add credentials input support to Run UX in Builder - Pass run inputs instead of storing them on the input nodes - Re-implement `RunnerInputUI` using `AgentRunDetailsView`; rename to `RunnerInputDialog` - Make `AgentRunDraftView` more flexible - Remove `RunnerInputList`, `RunnerInputBlock` - Make moving around in the Builder *smooooth* by reducing unnecessary re-renders - Clean up and partially re-write bead management logic - Replace `request*` fire-and-forget methods in `useAgentGraph` with direct action async callbacks - Clean up run input UI components - Simplify `RunnerUIWrapper` - Add `isEmpty` utility function in `@/lib/utils` (expanding on `_.isEmpty`) - Fix default value handling in `TypeBasedInput` (**Note:** after all the changes I've made I'm not sure this is still necessary) - Improve & clean up Builder test implementations Backend + API: - Fix front-end `Node`, `GraphMeta`, and `Block` types - Small refactor of `Graph` to match naming of some `LibraryAgent` attributes - Fix typing of `list_graphs`, `get_graph_meta_by_store_listing_version_id` endpoints - Add `GraphMeta` model and `GraphModel.meta()` shortcut - Move `POST /library/agents/{library_agent_id}/setup-trigger` to `POST /library/presets/setup-trigger` ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - Test the new functionality in the Builder: - [x] Running an agent with (credentials) inputs from the builder - [x] Beads behave correctly - [x] Running an agent without any inputs from the builder - [x] Scheduling an agent from the builder - [x] Adding and searching blocks in the block menu - [x] Test that all existing `AgentRunDraftView` functionality in the Library still works the same - [x] Run an agent - [x] Schedule an agent - [x] View past runs - [x] Run an agent with inputs, then edit the agent's inputs and view the agent in the Library (should be fine)
This commit is contained in:
committed by
GitHub
parent
309114a727
commit
36f5f24333
@@ -13,7 +13,7 @@ from prisma.types import (
|
||||
AgentNodeLinkCreateInput,
|
||||
StoreListingVersionWhereInput,
|
||||
)
|
||||
from pydantic import JsonValue, create_model
|
||||
from pydantic import Field, JsonValue, create_model
|
||||
from pydantic.fields import computed_field
|
||||
|
||||
from backend.blocks.agent import AgentExecutorBlock
|
||||
@@ -188,6 +188,23 @@ class BaseGraph(BaseDbModel):
|
||||
)
|
||||
)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def has_external_trigger(self) -> bool:
|
||||
return self.webhook_input_node is not None
|
||||
|
||||
@property
|
||||
def webhook_input_node(self) -> Node | None:
|
||||
return next(
|
||||
(
|
||||
node
|
||||
for node in self.nodes
|
||||
if node.block.block_type
|
||||
in (BlockType.WEBHOOK, BlockType.WEBHOOK_MANUAL)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _generate_schema(
|
||||
*props: tuple[type[AgentInputBlock.Input] | type[AgentOutputBlock.Input], dict],
|
||||
@@ -325,11 +342,6 @@ class GraphModel(Graph):
|
||||
user_id: str
|
||||
nodes: list[NodeModel] = [] # type: ignore
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def has_webhook_trigger(self) -> bool:
|
||||
return self.webhook_input_node is not None
|
||||
|
||||
@property
|
||||
def starting_nodes(self) -> list[NodeModel]:
|
||||
outbound_nodes = {link.sink_id for link in self.links}
|
||||
@@ -342,17 +354,12 @@ class GraphModel(Graph):
|
||||
if node.id not in outbound_nodes or node.id in input_nodes
|
||||
]
|
||||
|
||||
@property
|
||||
def webhook_input_node(self) -> NodeModel | None:
|
||||
return next(
|
||||
(
|
||||
node
|
||||
for node in self.nodes
|
||||
if node.block.block_type
|
||||
in (BlockType.WEBHOOK, BlockType.WEBHOOK_MANUAL)
|
||||
),
|
||||
None,
|
||||
)
|
||||
def meta(self) -> "GraphMeta":
|
||||
"""
|
||||
Returns a GraphMeta object with metadata about the graph.
|
||||
This is used to return metadata about the graph without exposing nodes and links.
|
||||
"""
|
||||
return GraphMeta.from_graph(self)
|
||||
|
||||
def reassign_ids(self, user_id: str, reassign_graph_id: bool = False):
|
||||
"""
|
||||
@@ -611,6 +618,18 @@ class GraphModel(Graph):
|
||||
)
|
||||
|
||||
|
||||
class GraphMeta(Graph):
|
||||
user_id: str
|
||||
|
||||
# Easy work-around to prevent exposing nodes and links in the API response
|
||||
nodes: list[NodeModel] = Field(default=[], exclude=True) # type: ignore
|
||||
links: list[Link] = Field(default=[], exclude=True)
|
||||
|
||||
@staticmethod
|
||||
def from_graph(graph: GraphModel) -> "GraphMeta":
|
||||
return GraphMeta(**graph.model_dump())
|
||||
|
||||
|
||||
# --------------------- CRUD functions --------------------- #
|
||||
|
||||
|
||||
@@ -639,10 +658,10 @@ async def set_node_webhook(node_id: str, webhook_id: str | None) -> NodeModel:
|
||||
return NodeModel.from_db(node)
|
||||
|
||||
|
||||
async def get_graphs(
|
||||
async def list_graphs(
|
||||
user_id: str,
|
||||
filter_by: Literal["active"] | None = "active",
|
||||
) -> list[GraphModel]:
|
||||
) -> list[GraphMeta]:
|
||||
"""
|
||||
Retrieves graph metadata objects.
|
||||
Default behaviour is to get all currently active graphs.
|
||||
@@ -652,7 +671,7 @@ async def get_graphs(
|
||||
user_id: The ID of the user that owns the graph.
|
||||
|
||||
Returns:
|
||||
list[GraphModel]: A list of objects representing the retrieved graphs.
|
||||
list[GraphMeta]: A list of objects representing the retrieved graphs.
|
||||
"""
|
||||
where_clause: AgentGraphWhereInput = {"userId": user_id}
|
||||
|
||||
@@ -666,13 +685,13 @@ async def get_graphs(
|
||||
include=AGENT_GRAPH_INCLUDE,
|
||||
)
|
||||
|
||||
graph_models = []
|
||||
graph_models: list[GraphMeta] = []
|
||||
for graph in graphs:
|
||||
try:
|
||||
graph_model = GraphModel.from_db(graph)
|
||||
# Trigger serialization to validate that the graph is well formed.
|
||||
graph_model.model_dump()
|
||||
graph_models.append(graph_model)
|
||||
graph_meta = GraphModel.from_db(graph).meta()
|
||||
# Trigger serialization to validate that the graph is well formed
|
||||
graph_meta.model_dump()
|
||||
graph_models.append(graph_meta)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing graph {graph.id}: {e}")
|
||||
continue
|
||||
|
||||
@@ -448,10 +448,10 @@ class DeleteGraphResponse(TypedDict):
|
||||
tags=["graphs"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def get_graphs(
|
||||
async def list_graphs(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> Sequence[graph_db.GraphModel]:
|
||||
return await graph_db.get_graphs(filter_by="active", user_id=user_id)
|
||||
) -> Sequence[graph_db.GraphMeta]:
|
||||
return await graph_db.list_graphs(filter_by="active", user_id=user_id)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
|
||||
@@ -270,7 +270,7 @@ def test_get_graphs(
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"backend.server.routers.v1.graph_db.get_graphs",
|
||||
"backend.server.routers.v1.graph_db.list_graphs",
|
||||
return_value=[mock_graph],
|
||||
)
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
|
||||
async def get_library_agent_by_store_version_id(
|
||||
store_listing_version_id: str,
|
||||
user_id: str,
|
||||
):
|
||||
) -> library_model.LibraryAgent | None:
|
||||
"""
|
||||
Get the library agent metadata for a given store listing version ID and user ID.
|
||||
"""
|
||||
@@ -202,7 +202,7 @@ async def get_library_agent_by_store_version_id(
|
||||
)
|
||||
if not store_listing_version:
|
||||
logger.warning(f"Store listing version not found: {store_listing_version_id}")
|
||||
raise store_exceptions.AgentNotFoundError(
|
||||
raise NotFoundError(
|
||||
f"Store listing version {store_listing_version_id} not found or invalid"
|
||||
)
|
||||
|
||||
@@ -214,12 +214,9 @@ async def get_library_agent_by_store_version_id(
|
||||
"agentGraphVersion": store_listing_version.agentGraphVersion,
|
||||
"isDeleted": False,
|
||||
},
|
||||
include={"AgentGraph": True},
|
||||
include=library_agent_include(user_id),
|
||||
)
|
||||
if agent:
|
||||
return library_model.LibraryAgent.from_db(agent)
|
||||
else:
|
||||
return None
|
||||
return library_model.LibraryAgent.from_db(agent) if agent else None
|
||||
|
||||
|
||||
async def get_library_agent_by_graph_id(
|
||||
|
||||
@@ -129,7 +129,7 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
credentials_input_schema=(
|
||||
graph.credentials_input_schema if sub_graphs is not None else None
|
||||
),
|
||||
has_external_trigger=graph.has_webhook_trigger,
|
||||
has_external_trigger=graph.has_external_trigger,
|
||||
trigger_setup_info=(
|
||||
LibraryAgentTriggerInfo(
|
||||
provider=trigger_block.webhook_config.provider,
|
||||
@@ -262,6 +262,19 @@ class LibraryAgentPresetUpdatable(pydantic.BaseModel):
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class TriggeredPresetSetupRequest(pydantic.BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
|
||||
trigger_config: dict[str, Any]
|
||||
agent_credentials: dict[str, CredentialsMetaInput] = pydantic.Field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
class LibraryAgentPreset(LibraryAgentPresetCreatable):
|
||||
"""Represents a preset configuration for a library agent."""
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
import autogpt_libs.auth as autogpt_auth_lib
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import backend.server.v2.library.db as library_db
|
||||
import backend.server.v2.library.model as library_model
|
||||
import backend.server.v2.store.exceptions as store_exceptions
|
||||
from backend.data.graph import get_graph
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.executor.utils import make_node_credentials_input_map
|
||||
from backend.integrations.webhooks.utils import setup_webhook_for_block
|
||||
from backend.util.exceptions import NotFoundError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -113,12 +108,11 @@ async def get_library_agent_by_graph_id(
|
||||
"/marketplace/{store_listing_version_id}",
|
||||
summary="Get Agent By Store ID",
|
||||
tags=["store, library"],
|
||||
response_model=library_model.LibraryAgent | None,
|
||||
)
|
||||
async def get_library_agent_by_store_listing_version_id(
|
||||
store_listing_version_id: str,
|
||||
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||
):
|
||||
) -> library_model.LibraryAgent | None:
|
||||
"""
|
||||
Get Library Agent from Store Listing Version ID.
|
||||
"""
|
||||
@@ -295,81 +289,3 @@ async def fork_library_agent(
|
||||
library_agent_id=library_agent_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
class TriggeredPresetSetupParams(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
trigger_config: dict[str, Any]
|
||||
agent_credentials: dict[str, CredentialsMetaInput] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@router.post("/{library_agent_id}/setup-trigger")
|
||||
async def setup_trigger(
|
||||
library_agent_id: str = Path(..., description="ID of the library agent"),
|
||||
params: TriggeredPresetSetupParams = Body(),
|
||||
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||
) -> library_model.LibraryAgentPreset:
|
||||
"""
|
||||
Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.
|
||||
Returns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.
|
||||
"""
|
||||
library_agent = await library_db.get_library_agent(
|
||||
id=library_agent_id, user_id=user_id
|
||||
)
|
||||
if not library_agent:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Library agent #{library_agent_id} not found",
|
||||
)
|
||||
|
||||
graph = await get_graph(
|
||||
library_agent.graph_id, version=library_agent.graph_version, user_id=user_id
|
||||
)
|
||||
if not graph:
|
||||
raise HTTPException(
|
||||
status.HTTP_410_GONE,
|
||||
f"Graph #{library_agent.graph_id} not accessible (anymore)",
|
||||
)
|
||||
if not (trigger_node := graph.webhook_input_node):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Graph #{library_agent.graph_id} does not have a webhook node",
|
||||
)
|
||||
|
||||
trigger_config_with_credentials = {
|
||||
**params.trigger_config,
|
||||
**(
|
||||
make_node_credentials_input_map(graph, params.agent_credentials).get(
|
||||
trigger_node.id
|
||||
)
|
||||
or {}
|
||||
),
|
||||
}
|
||||
|
||||
new_webhook, feedback = await setup_webhook_for_block(
|
||||
user_id=user_id,
|
||||
trigger_block=trigger_node.block,
|
||||
trigger_config=trigger_config_with_credentials,
|
||||
)
|
||||
if not new_webhook:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Could not set up webhook: {feedback}",
|
||||
)
|
||||
|
||||
new_preset = await library_db.create_preset(
|
||||
user_id=user_id,
|
||||
preset=library_model.LibraryAgentPresetCreatable(
|
||||
graph_id=library_agent.graph_id,
|
||||
graph_version=library_agent.graph_version,
|
||||
name=params.name,
|
||||
description=params.description,
|
||||
inputs=trigger_config_with_credentials,
|
||||
credentials=params.agent_credentials,
|
||||
webhook_id=new_webhook.id,
|
||||
is_active=True,
|
||||
),
|
||||
)
|
||||
return new_preset
|
||||
|
||||
@@ -138,6 +138,66 @@ async def create_preset(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/presets/setup-trigger")
|
||||
async def setup_trigger(
|
||||
params: models.TriggeredPresetSetupRequest = Body(),
|
||||
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||
) -> models.LibraryAgentPreset:
|
||||
"""
|
||||
Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`.
|
||||
Returns the correspondingly created `LibraryAgentPreset` with `webhook_id` set.
|
||||
"""
|
||||
graph = await get_graph(
|
||||
params.graph_id, version=params.graph_version, user_id=user_id
|
||||
)
|
||||
if not graph:
|
||||
raise HTTPException(
|
||||
status.HTTP_410_GONE,
|
||||
f"Graph #{params.graph_id} not accessible (anymore)",
|
||||
)
|
||||
if not (trigger_node := graph.webhook_input_node):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Graph #{params.graph_id} does not have a webhook node",
|
||||
)
|
||||
|
||||
trigger_config_with_credentials = {
|
||||
**params.trigger_config,
|
||||
**(
|
||||
make_node_credentials_input_map(graph, params.agent_credentials).get(
|
||||
trigger_node.id
|
||||
)
|
||||
or {}
|
||||
),
|
||||
}
|
||||
|
||||
new_webhook, feedback = await setup_webhook_for_block(
|
||||
user_id=user_id,
|
||||
trigger_block=trigger_node.block,
|
||||
trigger_config=trigger_config_with_credentials,
|
||||
)
|
||||
if not new_webhook:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Could not set up webhook: {feedback}",
|
||||
)
|
||||
|
||||
new_preset = await db.create_preset(
|
||||
user_id=user_id,
|
||||
preset=models.LibraryAgentPresetCreatable(
|
||||
graph_id=params.graph_id,
|
||||
graph_version=params.graph_version,
|
||||
name=params.name,
|
||||
description=params.description,
|
||||
inputs=trigger_config_with_credentials,
|
||||
credentials=params.agent_credentials,
|
||||
webhook_id=new_webhook.id,
|
||||
is_active=True,
|
||||
),
|
||||
)
|
||||
return new_preset
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/presets/{preset_id}",
|
||||
summary="Update an existing preset",
|
||||
|
||||
@@ -7,10 +7,15 @@ import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
|
||||
import backend.data.graph
|
||||
import backend.server.v2.store.exceptions
|
||||
import backend.server.v2.store.model
|
||||
from backend.data.graph import GraphModel, get_sub_graphs
|
||||
from backend.data.graph import (
|
||||
GraphMeta,
|
||||
GraphModel,
|
||||
get_graph,
|
||||
get_graph_as_admin,
|
||||
get_sub_graphs,
|
||||
)
|
||||
from backend.data.includes import AGENT_GRAPH_INCLUDE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -193,9 +198,7 @@ async def get_store_agent_details(
|
||||
) from e
|
||||
|
||||
|
||||
async def get_available_graph(
|
||||
store_listing_version_id: str,
|
||||
):
|
||||
async def get_available_graph(store_listing_version_id: str) -> GraphMeta:
|
||||
try:
|
||||
# Get avaialble, non-deleted store listing version
|
||||
store_listing_version = (
|
||||
@@ -215,18 +218,7 @@ async def get_available_graph(
|
||||
detail=f"Store listing version {store_listing_version_id} not found",
|
||||
)
|
||||
|
||||
graph = GraphModel.from_db(store_listing_version.AgentGraph)
|
||||
# We return graph meta, without nodes, they cannot be just removed
|
||||
# because then input_schema would be empty
|
||||
return {
|
||||
"id": graph.id,
|
||||
"version": graph.version,
|
||||
"is_active": graph.is_active,
|
||||
"name": graph.name,
|
||||
"description": graph.description,
|
||||
"input_schema": graph.input_schema,
|
||||
"output_schema": graph.output_schema,
|
||||
}
|
||||
return GraphModel.from_db(store_listing_version.AgentGraph).meta()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting agent: {e}")
|
||||
@@ -1024,7 +1016,7 @@ async def get_agent(
|
||||
if not store_listing_version:
|
||||
raise ValueError(f"Store listing version {store_listing_version_id} not found")
|
||||
|
||||
graph = await backend.data.graph.get_graph(
|
||||
graph = await get_graph(
|
||||
user_id=user_id,
|
||||
graph_id=store_listing_version.agentGraphId,
|
||||
version=store_listing_version.agentGraphVersion,
|
||||
@@ -1383,7 +1375,7 @@ async def get_agent_as_admin(
|
||||
if not store_listing_version:
|
||||
raise ValueError(f"Store listing version {store_listing_version_id} not found")
|
||||
|
||||
graph = await backend.data.graph.get_graph_as_admin(
|
||||
graph = await get_graph_as_admin(
|
||||
user_id=user_id,
|
||||
graph_id=store_listing_version.agentGraphId,
|
||||
version=store_listing_version.agentGraphVersion,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"description": "A test graph",
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"has_webhook_trigger": false,
|
||||
"has_external_trigger": false,
|
||||
"id": "graph-123",
|
||||
"input_schema": {
|
||||
"properties": {},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"description": "A test graph",
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"has_webhook_trigger": false,
|
||||
"has_external_trigger": false,
|
||||
"id": "graph-123",
|
||||
"input_schema": {
|
||||
"properties": {},
|
||||
@@ -16,9 +16,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"is_active": true,
|
||||
"links": [],
|
||||
"name": "Test Graph",
|
||||
"nodes": [],
|
||||
"output_schema": {
|
||||
"properties": {},
|
||||
"required": [],
|
||||
|
||||
@@ -46,13 +46,13 @@ export default function Page() {
|
||||
setStoreAgent(storeAgent);
|
||||
});
|
||||
api
|
||||
.getAgentMetaByStoreListingVersionId(state?.selectedStoreListingVersionId)
|
||||
.getGraphMetaByStoreListingVersionID(state.selectedStoreListingVersionId)
|
||||
.then((agent) => {
|
||||
setAgent(agent);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const update: { [key: string]: any } = {};
|
||||
// Set default values from schema
|
||||
Object.entries(agent.input_schema?.properties || {}).forEach(
|
||||
Object.entries(agent.input_schema.properties).forEach(
|
||||
([key, value]) => {
|
||||
// Skip if already set
|
||||
if (state.agentInput && state.agentInput[key]) {
|
||||
@@ -224,7 +224,7 @@ export default function Page() {
|
||||
<CardTitle className="font-poppins text-lg">Input</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{Object.entries(agent?.input_schema?.properties || {}).map(
|
||||
{Object.entries(agent?.input_schema.properties || {}).map(
|
||||
([key, inputSubSchema]) => (
|
||||
<div key={key} className="flex flex-col space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
|
||||
@@ -512,7 +512,8 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
) : selectedView.type == "run" ? (
|
||||
/* Draft new runs / Create new presets */
|
||||
<AgentRunDraftView
|
||||
agent={agent}
|
||||
graph={graph}
|
||||
triggerSetupInfo={agent.trigger_setup_info}
|
||||
onRun={selectRun}
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onCreatePreset={onCreatePreset}
|
||||
@@ -521,7 +522,8 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
) : selectedView.type == "preset" ? (
|
||||
/* Edit & update presets */
|
||||
<AgentRunDraftView
|
||||
agent={agent}
|
||||
graph={graph}
|
||||
triggerSetupInfo={agent.trigger_setup_info}
|
||||
agentPreset={
|
||||
agentPresets.find((preset) => preset.id == selectedView.id)!
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
@@ -18,8 +24,8 @@ export type CustomEdgeData = {
|
||||
edgeColor: string;
|
||||
sourcePos?: XYPosition;
|
||||
isStatic?: boolean;
|
||||
beadUp?: number;
|
||||
beadDown?: number;
|
||||
beadUp: number;
|
||||
beadDown: number;
|
||||
beadData?: Map<string, NodeExecutionResult["status"]>;
|
||||
};
|
||||
|
||||
@@ -46,6 +52,7 @@ export function CustomEdge({
|
||||
created: number;
|
||||
destroyed: number;
|
||||
}>({ beads: [], created: 0, destroyed: 0 });
|
||||
const beadsRef = useRef(beads);
|
||||
const { svgPath, length, getPointForT, getTForDistance } = useBezierPath(
|
||||
sourceX - 5,
|
||||
sourceY - 5,
|
||||
@@ -87,89 +94,80 @@ export function CustomEdge({
|
||||
[getTForDistance, length, visualizeBeads],
|
||||
);
|
||||
|
||||
beadsRef.current = beads;
|
||||
useEffect(() => {
|
||||
if (data?.beadUp === 0 && data?.beadDown === 0) {
|
||||
const beadUp: number = data?.beadUp ?? 0;
|
||||
const beadDown: number = data?.beadDown ?? 0;
|
||||
|
||||
if (
|
||||
beadUp === 0 &&
|
||||
beadDown === 0 &&
|
||||
(beads.created > 0 || beads.destroyed > 0)
|
||||
) {
|
||||
setBeads({ beads: [], created: 0, destroyed: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const beadUp: number = data?.beadUp ?? 0;
|
||||
|
||||
// Add beads
|
||||
setBeads(({ beads, created, destroyed }) => {
|
||||
const newBeads = [];
|
||||
for (let i = 0; i < beadUp - created; i++) {
|
||||
newBeads.push({ t: 0, targetT: 0, startTime: Date.now() });
|
||||
}
|
||||
|
||||
const b = setTargetPositions([...beads, ...newBeads]);
|
||||
return { beads: b, created: beadUp, destroyed };
|
||||
});
|
||||
|
||||
// Remove beads if not animating
|
||||
if (visualizeBeads !== "animate") {
|
||||
if (beadUp > beads.created) {
|
||||
setBeads(({ beads, created, destroyed }) => {
|
||||
let destroyedCount = 0;
|
||||
const newBeads = [];
|
||||
for (let i = 0; i < beadUp - created; i++) {
|
||||
newBeads.push({ t: 0, targetT: 0, startTime: Date.now() });
|
||||
}
|
||||
|
||||
const newBeads = beads
|
||||
.map((bead) => ({ ...bead }))
|
||||
.filter((bead, index) => {
|
||||
const beadDown: number = data?.beadDown ?? 0;
|
||||
const removeCount = beadDown - destroyed;
|
||||
if (bead.t >= bead.targetT && index < removeCount) {
|
||||
destroyedCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
beads: setTargetPositions(newBeads),
|
||||
created,
|
||||
destroyed: destroyed + destroyedCount,
|
||||
};
|
||||
const b = setTargetPositions([...beads, ...newBeads]);
|
||||
return { beads: b, created: beadUp, destroyed };
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate and remove beads
|
||||
const interval = setInterval(() => {
|
||||
setBeads(({ beads, created, destroyed }) => {
|
||||
let destroyedCount = 0;
|
||||
const interval = setInterval(
|
||||
({ current: beads }) => {
|
||||
// If there are no beads visible or moving, stop re-rendering
|
||||
if (
|
||||
(beadUp === beads.created && beads.created === beads.destroyed) ||
|
||||
beads.beads.every((bead) => bead.t >= bead.targetT)
|
||||
) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
const newBeads = beads
|
||||
.map((bead) => {
|
||||
const progressIncrement = deltaTime / animationDuration;
|
||||
const t = Math.min(
|
||||
bead.t + bead.targetT * progressIncrement,
|
||||
bead.targetT,
|
||||
);
|
||||
setBeads(({ beads, created, destroyed }) => {
|
||||
let destroyedCount = 0;
|
||||
|
||||
return {
|
||||
...bead,
|
||||
t,
|
||||
};
|
||||
})
|
||||
.filter((bead, index) => {
|
||||
const beadDown: number = data?.beadDown ?? 0;
|
||||
const removeCount = beadDown - destroyed;
|
||||
if (bead.t >= bead.targetT && index < removeCount) {
|
||||
destroyedCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const newBeads = beads
|
||||
.map((bead) => {
|
||||
const progressIncrement = deltaTime / animationDuration;
|
||||
const t = Math.min(
|
||||
bead.t + bead.targetT * progressIncrement,
|
||||
bead.targetT,
|
||||
);
|
||||
|
||||
return {
|
||||
beads: setTargetPositions(newBeads),
|
||||
created,
|
||||
destroyed: destroyed + destroyedCount,
|
||||
};
|
||||
});
|
||||
}, deltaTime);
|
||||
return { ...bead, t };
|
||||
})
|
||||
.filter((bead, index) => {
|
||||
const removeCount = beadDown - destroyed;
|
||||
if (bead.t >= bead.targetT && index < removeCount) {
|
||||
destroyedCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
beads: setTargetPositions(newBeads),
|
||||
created,
|
||||
destroyed: destroyed + destroyedCount,
|
||||
};
|
||||
});
|
||||
},
|
||||
deltaTime,
|
||||
beadsRef,
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [data, setTargetPositions, visualizeBeads]);
|
||||
}, [data?.beadUp, data?.beadDown, setTargetPositions, visualizeBeads]);
|
||||
|
||||
const middle = getPointForT(0.5);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
useReactFlow,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
useViewport,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
@@ -53,7 +52,6 @@ import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import RunnerUIWrapper, {
|
||||
RunnerUIWrapperRef,
|
||||
} from "@/components/RunnerUIWrapper";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import OttoChatWidget from "@/components/OttoChatWidget";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
@@ -91,6 +89,7 @@ const FlowEditor: React.FC<{
|
||||
getNode,
|
||||
deleteElements,
|
||||
updateNode,
|
||||
getViewport,
|
||||
setViewport,
|
||||
} = useReactFlow<CustomNode, CustomEdge>();
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
@@ -105,18 +104,17 @@ const FlowEditor: React.FC<{
|
||||
agentDescription,
|
||||
setAgentDescription,
|
||||
savedAgent,
|
||||
availableNodes,
|
||||
availableBlocks,
|
||||
availableFlows,
|
||||
getOutputType,
|
||||
requestSave,
|
||||
requestSaveAndRun,
|
||||
requestStopRun,
|
||||
scheduleRunner,
|
||||
saveAgent,
|
||||
saveAndRun,
|
||||
stopRun,
|
||||
createRunSchedule,
|
||||
isSaving,
|
||||
isRunning,
|
||||
isStopping,
|
||||
isScheduling,
|
||||
setIsScheduling,
|
||||
nodes,
|
||||
setNodes,
|
||||
edges,
|
||||
@@ -157,8 +155,6 @@ const FlowEditor: React.FC<{
|
||||
|
||||
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
|
||||
|
||||
const [openCron, setOpenCron] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const TUTORIAL_STORAGE_KEY = "shepherd-tour";
|
||||
@@ -193,19 +189,11 @@ const FlowEditor: React.FC<{
|
||||
startTutorial(emptyNodes, setPinBlocksPopover, setPinSavePopover);
|
||||
localStorage.setItem(TUTORIAL_STORAGE_KEY, "yes");
|
||||
}
|
||||
}, [
|
||||
availableNodes,
|
||||
router,
|
||||
pathname,
|
||||
params,
|
||||
setEdges,
|
||||
setNodes,
|
||||
nodes.length,
|
||||
]);
|
||||
}, [router, pathname, params, setEdges, setNodes, nodes.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.get("open_scheduling") === "true") {
|
||||
setOpenCron(true);
|
||||
runnerUIRef.current?.openRunInputDialog();
|
||||
}
|
||||
setFlowExecutionID(
|
||||
(params.get("flowExecutionID") as GraphExecutionID) || undefined,
|
||||
@@ -223,12 +211,12 @@ const FlowEditor: React.FC<{
|
||||
|
||||
if (isUndo) {
|
||||
event.preventDefault();
|
||||
handleUndo();
|
||||
history.undo();
|
||||
}
|
||||
|
||||
if (isRedo) {
|
||||
event.preventDefault();
|
||||
handleRedo();
|
||||
history.redo();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,18 +262,16 @@ const FlowEditor: React.FC<{
|
||||
// Function to clear status, output, and close the output info dropdown of all nodes
|
||||
// and reset data beads on edges
|
||||
const clearNodesStatusAndOutput = useCallback(() => {
|
||||
setNodes((nds) => {
|
||||
const newNodes = nds.map((node) => ({
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
status: undefined,
|
||||
isOutputOpen: false,
|
||||
},
|
||||
}));
|
||||
|
||||
return newNodes;
|
||||
});
|
||||
})),
|
||||
);
|
||||
}, [setNodes]);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
@@ -352,6 +338,8 @@ const FlowEditor: React.FC<{
|
||||
edgeColor,
|
||||
sourcePos: sourceNode!.position,
|
||||
isStatic: sourceNode!.data.isOutputStatic,
|
||||
beadUp: 0,
|
||||
beadDown: 0,
|
||||
},
|
||||
...connection,
|
||||
source: connection.source!,
|
||||
@@ -463,10 +451,9 @@ const FlowEditor: React.FC<{
|
||||
return uuidv4();
|
||||
}, []);
|
||||
|
||||
const { x, y, zoom } = useViewport();
|
||||
|
||||
// Set the initial view port to center the canvas.
|
||||
useEffect(() => {
|
||||
const { x, y } = getViewport();
|
||||
if (nodes.length <= 0 || x !== 0 || y !== 0) {
|
||||
return;
|
||||
}
|
||||
@@ -492,11 +479,11 @@ const FlowEditor: React.FC<{
|
||||
y: window.innerHeight / 2 - centerY * zoom,
|
||||
zoom: zoom,
|
||||
});
|
||||
}, [nodes, setViewport, x, y]);
|
||||
}, [nodes, getViewport, setViewport]);
|
||||
|
||||
const addNode = useCallback(
|
||||
(blockId: string, nodeType: string, hardcodedValues: any = {}) => {
|
||||
const nodeSchema = availableNodes.find((node) => node.id === blockId);
|
||||
const nodeSchema = availableBlocks.find((node) => node.id === blockId);
|
||||
if (!nodeSchema) {
|
||||
console.error(`Schema not found for block ID: ${blockId}`);
|
||||
return;
|
||||
@@ -513,6 +500,7 @@ const FlowEditor: React.FC<{
|
||||
|
||||
// Alternative: We could also use D3 force, Intersection for this (React flow Pro examples)
|
||||
|
||||
const { x, y } = getViewport();
|
||||
const viewportCoordinates =
|
||||
nodeDimensions && Object.keys(nodeDimensions).length > 0
|
||||
? // we will get all the dimension of nodes, then store
|
||||
@@ -573,14 +561,13 @@ const FlowEditor: React.FC<{
|
||||
},
|
||||
[
|
||||
nodeId,
|
||||
getViewport,
|
||||
setViewport,
|
||||
availableNodes,
|
||||
availableBlocks,
|
||||
addNodes,
|
||||
nodeDimensions,
|
||||
deleteElements,
|
||||
clearNodesStatusAndOutput,
|
||||
x,
|
||||
y,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -593,6 +580,8 @@ const FlowEditor: React.FC<{
|
||||
const rect = nodeElement.getBoundingClientRect();
|
||||
const { left, top, width, height } = rect;
|
||||
|
||||
const { x, y, zoom } = getViewport();
|
||||
|
||||
// Convert screen coordinates to flow coordinates
|
||||
const flowX = (left - x) / zoom;
|
||||
const flowY = (top - y) / zoom;
|
||||
@@ -610,20 +599,12 @@ const FlowEditor: React.FC<{
|
||||
}, {} as NodeDimension);
|
||||
|
||||
setNodeDimensions(newNodeDimensions);
|
||||
}, [nodes, x, y, zoom]);
|
||||
}, [nodes, getViewport]);
|
||||
|
||||
useEffect(() => {
|
||||
findNodeDimensions();
|
||||
}, [nodes, findNodeDimensions]);
|
||||
|
||||
const handleUndo = () => {
|
||||
history.undo();
|
||||
};
|
||||
|
||||
const handleRedo = () => {
|
||||
history.redo();
|
||||
};
|
||||
|
||||
const handleCopyPaste = useCopyPaste(getNextNodeId);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
@@ -653,39 +634,45 @@ const FlowEditor: React.FC<{
|
||||
clearNodesStatusAndOutput();
|
||||
}, [clearNodesStatusAndOutput]);
|
||||
|
||||
const editorControls: Control[] = [
|
||||
{
|
||||
label: "Undo",
|
||||
icon: <IconUndo2 />,
|
||||
onClick: handleUndo,
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
icon: <IconRedo2 />,
|
||||
onClick: handleRedo,
|
||||
},
|
||||
];
|
||||
const editorControls: Control[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "Undo",
|
||||
icon: <IconUndo2 />,
|
||||
onClick: history.undo,
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
icon: <IconRedo2 />,
|
||||
onClick: history.redo,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
// This function is called after cron expression is created
|
||||
// So you can collect inputs for scheduling
|
||||
const afterCronCreation = (cronExpression: string, scheduleName: string) => {
|
||||
runnerUIRef.current?.collectInputsForScheduling(
|
||||
cronExpression,
|
||||
scheduleName,
|
||||
);
|
||||
};
|
||||
|
||||
// This function Opens up form for creating cron expression
|
||||
const handleScheduleButton = () => {
|
||||
const handleRunButton = useCallback(async () => {
|
||||
if (isRunning) return;
|
||||
if (!savedAgent) {
|
||||
toast({
|
||||
title: `Please save the agent using the button in the left sidebar before running it.`,
|
||||
duration: 2000,
|
||||
title: `Please save the agent first, using the button in the left sidebar.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setOpenCron(true);
|
||||
};
|
||||
await saveAgent();
|
||||
runnerUIRef.current?.runOrOpenInput();
|
||||
}, [isRunning, savedAgent, toast, saveAgent]);
|
||||
|
||||
const handleScheduleButton = useCallback(async () => {
|
||||
if (isScheduling) return;
|
||||
if (!savedAgent) {
|
||||
toast({
|
||||
title: `Please save the agent first, using the button in the left sidebar.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await saveAgent();
|
||||
runnerUIRef.current?.openRunInputDialog();
|
||||
}, [isScheduling, savedAgent, toast, saveAgent]);
|
||||
|
||||
return (
|
||||
<FlowContext.Provider
|
||||
@@ -717,7 +704,7 @@ const FlowEditor: React.FC<{
|
||||
topChildren={
|
||||
<BlocksControl
|
||||
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
|
||||
blocks={availableNodes}
|
||||
blocks={availableBlocks}
|
||||
addBlock={addNode}
|
||||
flows={availableFlows}
|
||||
nodes={nodes}
|
||||
@@ -727,7 +714,7 @@ const FlowEditor: React.FC<{
|
||||
<SaveControl
|
||||
agentMeta={savedAgent}
|
||||
canSave={!isSaving && !isRunning && !isStopping}
|
||||
onSave={() => requestSave()}
|
||||
onSave={saveAgent}
|
||||
agentDescription={agentDescription}
|
||||
onDescriptionChange={setAgentDescription}
|
||||
agentName={agentName}
|
||||
@@ -735,38 +722,17 @@ const FlowEditor: React.FC<{
|
||||
pinSavePopover={pinSavePopover}
|
||||
/>
|
||||
}
|
||||
></ControlPanel>
|
||||
/>
|
||||
{!graphHasWebhookNodes ? (
|
||||
<>
|
||||
<PrimaryActionBar
|
||||
className="absolute bottom-0 left-1/2 z-20 -translate-x-1/2"
|
||||
onClickAgentOutputs={() =>
|
||||
runnerUIRef.current?.openRunnerOutput()
|
||||
}
|
||||
onClickRunAgent={() => {
|
||||
if (isRunning) return;
|
||||
if (!savedAgent) {
|
||||
toast({
|
||||
title: `Please save the agent using the button in the left sidebar before running it.`,
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
runnerUIRef.current?.runOrOpenInput();
|
||||
}}
|
||||
onClickStopRun={requestStopRun}
|
||||
onClickScheduleButton={handleScheduleButton}
|
||||
isScheduling={isScheduling}
|
||||
isDisabled={!savedAgent}
|
||||
isRunning={isRunning}
|
||||
/>
|
||||
<CronSchedulerDialog
|
||||
afterCronCreation={afterCronCreation}
|
||||
open={openCron}
|
||||
setOpen={setOpenCron}
|
||||
defaultScheduleName={agentName}
|
||||
/>
|
||||
</>
|
||||
<PrimaryActionBar
|
||||
className="absolute bottom-0 left-1/2 z-20 -translate-x-1/2"
|
||||
onClickAgentOutputs={runnerUIRef.current?.openRunnerOutput}
|
||||
onClickRunAgent={handleRunButton}
|
||||
onClickStopRun={stopRun}
|
||||
onClickScheduleButton={handleScheduleButton}
|
||||
isDisabled={!savedAgent}
|
||||
isRunning={isRunning}
|
||||
/>
|
||||
) : (
|
||||
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">
|
||||
<AlertTitle>You are building a Trigger Agent</AlertTitle>
|
||||
@@ -794,16 +760,15 @@ const FlowEditor: React.FC<{
|
||||
)}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<RunnerUIWrapper
|
||||
ref={runnerUIRef}
|
||||
nodes={nodes}
|
||||
setNodes={setNodes}
|
||||
setIsScheduling={setIsScheduling}
|
||||
isScheduling={isScheduling}
|
||||
isRunning={isRunning}
|
||||
scheduleRunner={scheduleRunner}
|
||||
requestSaveAndRun={requestSaveAndRun}
|
||||
/>
|
||||
{savedAgent && (
|
||||
<RunnerUIWrapper
|
||||
ref={runnerUIRef}
|
||||
graph={savedAgent}
|
||||
nodes={nodes}
|
||||
createRunSchedule={createRunSchedule}
|
||||
saveAndRun={saveAndRun}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
<OttoChatWidget
|
||||
graphID={flowID}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
import { Clock, LogOut } from "lucide-react";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { ClockIcon } from "@phosphor-icons/react";
|
||||
import { IconPlay, IconSquare } from "@/components/ui/icons";
|
||||
|
||||
interface PrimaryActionBarProps {
|
||||
onClickAgentOutputs: () => void;
|
||||
onClickAgentOutputs?: () => void;
|
||||
onClickRunAgent?: () => void;
|
||||
onClickStopRun: () => void;
|
||||
onClickScheduleButton?: () => void;
|
||||
isRunning: boolean;
|
||||
isDisabled: boolean;
|
||||
isScheduling: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -23,7 +22,6 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
onClickScheduleButton,
|
||||
isRunning,
|
||||
isDisabled,
|
||||
isScheduling,
|
||||
className,
|
||||
}) => {
|
||||
const buttonClasses =
|
||||
@@ -36,15 +34,17 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-1 md:gap-4">
|
||||
<Button
|
||||
className={buttonClasses}
|
||||
variant="outline"
|
||||
size="primary"
|
||||
onClick={onClickAgentOutputs}
|
||||
title="View agent outputs"
|
||||
>
|
||||
<LogOut className="hidden size-5 md:flex" /> Agent Outputs
|
||||
</Button>
|
||||
{onClickAgentOutputs && (
|
||||
<Button
|
||||
className={buttonClasses}
|
||||
variant="outline"
|
||||
size="primary"
|
||||
onClick={onClickAgentOutputs}
|
||||
title="View agent outputs"
|
||||
>
|
||||
<LogOut className="hidden size-5 md:flex" /> Agent Outputs
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isRunning ? (
|
||||
<Button
|
||||
@@ -82,15 +82,10 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
variant="outline"
|
||||
size="primary"
|
||||
onClick={onClickScheduleButton}
|
||||
disabled={isScheduling}
|
||||
title="Set up a run schedule for the agent"
|
||||
data-id="primary-action-schedule-agent"
|
||||
>
|
||||
{isScheduling ? (
|
||||
<FaSpinner className="animate-spin" />
|
||||
) : (
|
||||
<Clock className="hidden h-5 w-5 md:flex" />
|
||||
)}
|
||||
<ClockIcon className="hidden h-5 w-5 md:flex" />
|
||||
Schedule Run
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,215 +1,108 @@
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import RunnerOutputUI, { BlockOutput } from "./runner-ui/RunnerOutputUI";
|
||||
import RunnerInputUI from "./runner-ui/RunnerInputUI";
|
||||
import { Node } from "@xyflow/react";
|
||||
import { filterBlocksByType } from "@/lib/utils";
|
||||
import { CustomNodeData } from "@/components/CustomNode";
|
||||
import { RunnerInputDialog } from "@/components/runner-ui/RunnerInputUI";
|
||||
import {
|
||||
BlockIOObjectSubSchema,
|
||||
BlockIORootSchema,
|
||||
BlockUIType,
|
||||
CredentialsMetaInput,
|
||||
GraphMeta,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
|
||||
interface HardcodedValues {
|
||||
name: any;
|
||||
description: any;
|
||||
value: any;
|
||||
placeholder_values: any;
|
||||
}
|
||||
|
||||
export interface InputItem {
|
||||
id: string;
|
||||
type: "input";
|
||||
inputSchema: BlockIORootSchema;
|
||||
hardcodedValues: HardcodedValues;
|
||||
}
|
||||
import RunnerOutputUI, {
|
||||
OutputNodeInfo,
|
||||
} from "@/components/runner-ui/RunnerOutputUI";
|
||||
|
||||
interface RunnerUIWrapperProps {
|
||||
nodes: Node[];
|
||||
setNodes: React.Dispatch<React.SetStateAction<CustomNode[]>>;
|
||||
setIsScheduling: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isRunning: boolean;
|
||||
isScheduling: boolean;
|
||||
requestSaveAndRun: () => void;
|
||||
scheduleRunner: (
|
||||
graph: GraphMeta;
|
||||
nodes: Node<CustomNodeData>[];
|
||||
saveAndRun: (
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => void;
|
||||
createRunSchedule: (
|
||||
cronExpression: string,
|
||||
input: InputItem[],
|
||||
scheduleName: string,
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RunnerUIWrapperRef {
|
||||
openRunnerInput: () => void;
|
||||
openRunInputDialog: () => void;
|
||||
openRunnerOutput: () => void;
|
||||
runOrOpenInput: () => void;
|
||||
collectInputsForScheduling: (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
(
|
||||
{
|
||||
nodes,
|
||||
setIsScheduling,
|
||||
setNodes,
|
||||
isScheduling,
|
||||
isRunning,
|
||||
requestSaveAndRun,
|
||||
scheduleRunner,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isRunnerInputOpen, setIsRunnerInputOpen] = useState(false);
|
||||
({ graph, nodes, saveAndRun, createRunSchedule }, ref) => {
|
||||
const [isRunInputDialogOpen, setIsRunInputDialogOpen] = useState(false);
|
||||
const [isRunnerOutputOpen, setIsRunnerOutputOpen] = useState(false);
|
||||
const [scheduledInput, setScheduledInput] = useState(false);
|
||||
const [cronExpression, setCronExpression] = useState("");
|
||||
const [scheduleName, setScheduleName] = useState("");
|
||||
|
||||
const getBlockInputsAndOutputs = useCallback((): {
|
||||
inputs: InputItem[];
|
||||
outputs: BlockOutput[];
|
||||
} => {
|
||||
const inputBlocks = filterBlocksByType(
|
||||
nodes,
|
||||
(node) => node.data.uiType === BlockUIType.INPUT,
|
||||
);
|
||||
const graphInputs = graph.input_schema.properties;
|
||||
|
||||
const outputBlocks = filterBlocksByType(
|
||||
nodes,
|
||||
const graphOutputs = useMemo((): OutputNodeInfo[] => {
|
||||
const outputNodes = nodes.filter(
|
||||
(node) => node.data.uiType === BlockUIType.OUTPUT,
|
||||
);
|
||||
|
||||
const inputs = inputBlocks.map(
|
||||
(node) =>
|
||||
({
|
||||
id: node.id,
|
||||
type: "input" as const,
|
||||
inputSchema: (node.data.inputSchema as BlockIOObjectSubSchema)
|
||||
.properties.value as BlockIORootSchema,
|
||||
hardcodedValues: {
|
||||
name: (node.data.hardcodedValues as any).name || "",
|
||||
description: (node.data.hardcodedValues as any).description || "",
|
||||
value: (node.data.hardcodedValues as any).value,
|
||||
placeholder_values:
|
||||
(node.data.hardcodedValues as any).placeholder_values || [],
|
||||
},
|
||||
}) satisfies InputItem,
|
||||
);
|
||||
|
||||
const outputs = outputBlocks.map(
|
||||
return outputNodes.map(
|
||||
(node) =>
|
||||
({
|
||||
metadata: {
|
||||
name: (node.data.hardcodedValues as any).name || "Output",
|
||||
name: node.data.hardcodedValues.name || "Output",
|
||||
description:
|
||||
(node.data.hardcodedValues as any).description ||
|
||||
node.data.hardcodedValues.description ||
|
||||
"Output from the agent",
|
||||
},
|
||||
result:
|
||||
(node.data.executionResults as any)
|
||||
?.map((result: any) => result?.data?.output)
|
||||
.join("\n--\n") || "No output yet",
|
||||
}) satisfies BlockOutput,
|
||||
}) satisfies OutputNodeInfo,
|
||||
);
|
||||
|
||||
return { inputs, outputs };
|
||||
}, [nodes]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(nodeId: string, field: string, value: any) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
hardcodedValues: {
|
||||
...(node.data.hardcodedValues as any),
|
||||
[field]: value,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setNodes],
|
||||
);
|
||||
|
||||
const openRunnerInput = () => setIsRunnerInputOpen(true);
|
||||
const openRunInputDialog = () => setIsRunInputDialogOpen(true);
|
||||
const openRunnerOutput = () => setIsRunnerOutputOpen(true);
|
||||
|
||||
const runOrOpenInput = () => {
|
||||
const { inputs } = getBlockInputsAndOutputs();
|
||||
if (inputs.length > 0) {
|
||||
openRunnerInput();
|
||||
if (
|
||||
Object.keys(graphInputs).length > 0 ||
|
||||
Object.keys(graph.credentials_input_schema.properties).length > 0
|
||||
) {
|
||||
openRunInputDialog();
|
||||
} else {
|
||||
requestSaveAndRun();
|
||||
saveAndRun({}, {});
|
||||
}
|
||||
};
|
||||
|
||||
const collectInputsForScheduling = (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
) => {
|
||||
const { inputs } = getBlockInputsAndOutputs();
|
||||
setCronExpression(cronExpression);
|
||||
setScheduleName(scheduleName);
|
||||
|
||||
if (inputs.length > 0) {
|
||||
setScheduledInput(true);
|
||||
setIsRunnerInputOpen(true);
|
||||
} else {
|
||||
scheduleRunner(cronExpression, [], scheduleName);
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openRunnerInput,
|
||||
openRunnerOutput,
|
||||
runOrOpenInput,
|
||||
collectInputsForScheduling,
|
||||
}));
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() =>
|
||||
({
|
||||
openRunInputDialog,
|
||||
openRunnerOutput,
|
||||
runOrOpenInput,
|
||||
}) satisfies RunnerUIWrapperRef,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RunnerInputUI
|
||||
isOpen={isRunnerInputOpen}
|
||||
onClose={() => setIsRunnerInputOpen(false)}
|
||||
blockInputs={getBlockInputsAndOutputs().inputs}
|
||||
onInputChange={handleInputChange}
|
||||
onRun={() => {
|
||||
setIsRunnerInputOpen(false);
|
||||
requestSaveAndRun();
|
||||
}}
|
||||
scheduledInput={scheduledInput}
|
||||
isScheduling={isScheduling}
|
||||
onSchedule={async () => {
|
||||
setIsScheduling(true);
|
||||
await scheduleRunner(
|
||||
cronExpression,
|
||||
getBlockInputsAndOutputs().inputs,
|
||||
scheduleName,
|
||||
);
|
||||
setIsScheduling(false);
|
||||
setIsRunnerInputOpen(false);
|
||||
setScheduledInput(false);
|
||||
}}
|
||||
isRunning={isRunning}
|
||||
<RunnerInputDialog
|
||||
isOpen={isRunInputDialogOpen}
|
||||
doClose={() => setIsRunInputDialogOpen(false)}
|
||||
graph={graph}
|
||||
doRun={saveAndRun}
|
||||
doCreateSchedule={createRunSchedule}
|
||||
/>
|
||||
<RunnerOutputUI
|
||||
isOpen={isRunnerOutputOpen}
|
||||
onClose={() => setIsRunnerOutputOpen(false)}
|
||||
blockOutputs={getBlockInputsAndOutputs().outputs}
|
||||
doClose={() => setIsRunnerOutputOpen(false)}
|
||||
outputs={graphOutputs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function AgentRunDetailsView({
|
||||
] satisfies ButtonAction[])
|
||||
: []),
|
||||
...(["success", "failed", "stopped"].includes(runStatus) &&
|
||||
!graph.has_webhook_trigger &&
|
||||
!graph.has_external_trigger &&
|
||||
isEmpty(graph.credentials_input_schema.required) // TODO: enable re-run with credentials - https://linear.app/autogpt/issue/SECRT-1243
|
||||
? [
|
||||
{
|
||||
@@ -198,8 +198,8 @@ export default function AgentRunDetailsView({
|
||||
runAgain,
|
||||
stopRun,
|
||||
deleteRun,
|
||||
graph.has_webhook_trigger,
|
||||
graph.credentials_input_schema?.properties,
|
||||
graph.has_external_trigger,
|
||||
graph.credentials_input_schema.required,
|
||||
agent.can_access_graph,
|
||||
run.graph_id,
|
||||
run.graph_version,
|
||||
|
||||
@@ -4,48 +4,65 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CredentialsMetaInput,
|
||||
GraphExecutionID,
|
||||
LibraryAgent,
|
||||
GraphMeta,
|
||||
LibraryAgentPreset,
|
||||
LibraryAgentPresetID,
|
||||
LibraryAgentPresetUpdatable,
|
||||
LibraryAgentTriggerInfo,
|
||||
Schedule,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
import ActionButtonGroup from "@/components/agptui/action-button-group";
|
||||
import type { ButtonAction } from "@/components/agptui/types";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { CredentialsInput } from "@/components/integrations/credentials-input";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import SchemaTooltip from "@/components/SchemaTooltip";
|
||||
import { TypeBasedInput } from "@/components/type-based-input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { IconCross, IconPlay, IconSave } from "@/components/ui/icons";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { CredentialsInput } from "@/components/integrations/credentials-input";
|
||||
import { TypeBasedInput } from "@/components/type-based-input";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import { cn, isEmpty } from "@/lib/utils";
|
||||
import SchemaTooltip from "@/components/SchemaTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
useToast,
|
||||
useToastOnFail,
|
||||
} from "@/components/molecules/Toast/use-toast";
|
||||
import { isEmpty } from "lodash";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
export default function AgentRunDraftView({
|
||||
agent,
|
||||
graph,
|
||||
agentPreset,
|
||||
triggerSetupInfo,
|
||||
doRun: _doRun,
|
||||
onRun,
|
||||
onCreatePreset,
|
||||
onUpdatePreset,
|
||||
doDeletePreset,
|
||||
doCreateSchedule: _doCreateSchedule,
|
||||
onCreateSchedule,
|
||||
agentActions,
|
||||
className,
|
||||
}: {
|
||||
agent: LibraryAgent;
|
||||
agentActions: ButtonAction[];
|
||||
onRun: (runID: GraphExecutionID) => void;
|
||||
onCreateSchedule: (schedule: Schedule) => void;
|
||||
graph: GraphMeta;
|
||||
triggerSetupInfo?: LibraryAgentTriggerInfo;
|
||||
agentActions?: ButtonAction[];
|
||||
doRun?: (
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => Promise<void>;
|
||||
onRun?: (runID: GraphExecutionID) => void;
|
||||
doCreateSchedule?: (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => Promise<void>;
|
||||
onCreateSchedule?: (schedule: Schedule) => void;
|
||||
className?: string;
|
||||
} & (
|
||||
| {
|
||||
onCreatePreset: (preset: LibraryAgentPreset) => void;
|
||||
onCreatePreset?: (preset: LibraryAgentPreset) => void;
|
||||
agentPreset?: never;
|
||||
onUpdatePreset?: never;
|
||||
doDeletePreset?: never;
|
||||
@@ -84,36 +101,26 @@ export default function AgentRunDraftView({
|
||||
}, [agentPreset]);
|
||||
|
||||
const agentInputSchema = useMemo(
|
||||
() =>
|
||||
agent.has_external_trigger
|
||||
? agent.trigger_setup_info.config_schema
|
||||
: agent.input_schema,
|
||||
[agent],
|
||||
() => triggerSetupInfo?.config_schema ?? graph.input_schema,
|
||||
[graph, triggerSetupInfo],
|
||||
);
|
||||
const agentInputFields = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(agentInputSchema?.properties || {}).filter(
|
||||
Object.entries(agentInputSchema.properties).filter(
|
||||
([_, subSchema]) => !subSchema.hidden,
|
||||
),
|
||||
),
|
||||
[agentInputSchema],
|
||||
);
|
||||
const agentCredentialsInputFields = useMemo(
|
||||
() => agent.credentials_input_schema?.properties || {},
|
||||
[agent],
|
||||
() => graph.credentials_input_schema.properties,
|
||||
[graph],
|
||||
);
|
||||
|
||||
const [allRequiredInputsAreSet, missingInputs] = useMemo(() => {
|
||||
const nonEmptyInputs = new Set(
|
||||
Object.keys(inputValues).filter((k) => {
|
||||
const value = inputValues[k];
|
||||
return (
|
||||
value !== undefined &&
|
||||
value !== "" &&
|
||||
(typeof value !== "object" || !isEmpty(value))
|
||||
);
|
||||
}),
|
||||
Object.keys(inputValues).filter((k) => !isEmpty(inputValues[k])),
|
||||
);
|
||||
const requiredInputs = new Set(
|
||||
agentInputSchema.required as string[] | undefined,
|
||||
@@ -135,7 +142,7 @@ export default function AgentRunDraftView({
|
||||
(needPresetName: boolean = true) => {
|
||||
const allMissingFields = (
|
||||
needPresetName && !presetName
|
||||
? [agent.has_external_trigger ? "trigger_name" : "preset_name"]
|
||||
? [graph.has_external_trigger ? "trigger_name" : "preset_name"]
|
||||
: []
|
||||
)
|
||||
.concat(missingInputs)
|
||||
@@ -148,29 +155,29 @@ export default function AgentRunDraftView({
|
||||
[missingInputs, missingCredentials],
|
||||
);
|
||||
|
||||
const doRun = useCallback(() => {
|
||||
const doRun = useCallback(async () => {
|
||||
// Manually running webhook-triggered agents is not supported
|
||||
if (agent.has_external_trigger) return;
|
||||
if (graph.has_external_trigger) return;
|
||||
|
||||
if (!agentPreset || changedPresetAttributes.size > 0) {
|
||||
if (!allRequiredInputsAreSet || !allCredentialsAreSet) {
|
||||
notifyMissingInputs(false);
|
||||
return;
|
||||
}
|
||||
if (_doRun) {
|
||||
await _doRun(inputValues, inputCredentials);
|
||||
return;
|
||||
}
|
||||
// TODO: on executing preset with changes, ask for confirmation and offer save+run
|
||||
api
|
||||
.executeGraph(
|
||||
agent.graph_id,
|
||||
agent.graph_version,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
)
|
||||
.then((newRun) => onRun(newRun.graph_exec_id))
|
||||
const newRun = await api
|
||||
.executeGraph(graph.id, graph.version, inputValues, inputCredentials)
|
||||
.catch(toastOnFail("execute agent"));
|
||||
|
||||
if (newRun && onRun) onRun(newRun.graph_exec_id);
|
||||
} else {
|
||||
api
|
||||
await api
|
||||
.executeLibraryAgentPreset(agentPreset.id)
|
||||
.then((newRun) => onRun(newRun.id))
|
||||
.then((newRun) => onRun && onRun(newRun.id))
|
||||
.catch(toastOnFail("execute agent preset"));
|
||||
}
|
||||
// Mark run agent onboarding step as completed
|
||||
@@ -179,7 +186,7 @@ export default function AgentRunDraftView({
|
||||
}
|
||||
}, [
|
||||
api,
|
||||
agent,
|
||||
graph,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
onRun,
|
||||
@@ -188,7 +195,7 @@ export default function AgentRunDraftView({
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
|
||||
const doCreatePreset = useCallback(() => {
|
||||
const doCreatePreset = useCallback(async () => {
|
||||
if (!onCreatePreset) return;
|
||||
|
||||
if (!presetName || !allRequiredInputsAreSet || !allCredentialsAreSet) {
|
||||
@@ -196,12 +203,12 @@ export default function AgentRunDraftView({
|
||||
return;
|
||||
}
|
||||
|
||||
api
|
||||
await api
|
||||
.createLibraryAgentPreset({
|
||||
name: presetName,
|
||||
description: presetDescription,
|
||||
graph_id: agent.graph_id,
|
||||
graph_version: agent.graph_version,
|
||||
graph_id: graph.id,
|
||||
graph_version: graph.version,
|
||||
inputs: inputValues,
|
||||
credentials: inputCredentials,
|
||||
})
|
||||
@@ -212,7 +219,7 @@ export default function AgentRunDraftView({
|
||||
.catch(toastOnFail("save agent preset"));
|
||||
}, [
|
||||
api,
|
||||
agent,
|
||||
graph,
|
||||
presetName,
|
||||
presetDescription,
|
||||
inputValues,
|
||||
@@ -224,7 +231,7 @@ export default function AgentRunDraftView({
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
|
||||
const doUpdatePreset = useCallback(() => {
|
||||
const doUpdatePreset = useCallback(async () => {
|
||||
if (!agentPreset || changedPresetAttributes.size == 0) return;
|
||||
|
||||
if (!presetName || !allRequiredInputsAreSet || !allCredentialsAreSet) {
|
||||
@@ -243,7 +250,7 @@ export default function AgentRunDraftView({
|
||||
updatePreset["inputs"] = inputValues;
|
||||
updatePreset["credentials"] = inputCredentials;
|
||||
}
|
||||
api
|
||||
await api
|
||||
.updateLibraryAgentPreset(agentPreset.id, updatePreset)
|
||||
.then((updatedPreset) => {
|
||||
onUpdatePreset(updatedPreset);
|
||||
@@ -252,7 +259,7 @@ export default function AgentRunDraftView({
|
||||
.catch(toastOnFail("update agent preset"));
|
||||
}, [
|
||||
api,
|
||||
agent,
|
||||
graph,
|
||||
presetName,
|
||||
presetDescription,
|
||||
inputValues,
|
||||
@@ -275,19 +282,16 @@ export default function AgentRunDraftView({
|
||||
[agentPreset, api, onUpdatePreset],
|
||||
);
|
||||
|
||||
const doSetupTrigger = useCallback(() => {
|
||||
const doSetupTrigger = useCallback(async () => {
|
||||
// Setting up a trigger for non-webhook-triggered agents is not supported
|
||||
if (!agent.has_external_trigger || !onCreatePreset) return;
|
||||
if (!triggerSetupInfo || !onCreatePreset) return;
|
||||
|
||||
if (!presetName || !allRequiredInputsAreSet || !allCredentialsAreSet) {
|
||||
notifyMissingInputs();
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialsInputName =
|
||||
agent.trigger_setup_info.credentials_input_name;
|
||||
|
||||
if (!credentialsInputName) {
|
||||
if (!triggerSetupInfo.credentials_input_name) {
|
||||
// FIXME: implement support for manual-setup webhooks
|
||||
toast({
|
||||
variant: "destructive",
|
||||
@@ -297,10 +301,12 @@ export default function AgentRunDraftView({
|
||||
return;
|
||||
}
|
||||
|
||||
api
|
||||
.setupAgentTrigger(agent.id, {
|
||||
await api
|
||||
.setupAgentTrigger({
|
||||
name: presetName,
|
||||
description: presetDescription,
|
||||
graph_id: graph.id,
|
||||
graph_version: graph.version,
|
||||
trigger_config: inputValues,
|
||||
agent_credentials: inputCredentials,
|
||||
})
|
||||
@@ -316,7 +322,7 @@ export default function AgentRunDraftView({
|
||||
}
|
||||
}, [
|
||||
api,
|
||||
agent,
|
||||
graph,
|
||||
presetName,
|
||||
presetDescription,
|
||||
inputValues,
|
||||
@@ -330,7 +336,7 @@ export default function AgentRunDraftView({
|
||||
|
||||
const openScheduleDialog = useCallback(() => {
|
||||
// Scheduling is not supported for webhook-triggered agents
|
||||
if (agent.has_external_trigger) return;
|
||||
if (graph.has_external_trigger) return;
|
||||
|
||||
if (!allRequiredInputsAreSet || !allCredentialsAreSet) {
|
||||
notifyMissingInputs(false);
|
||||
@@ -339,36 +345,46 @@ export default function AgentRunDraftView({
|
||||
|
||||
setCronScheduleDialogOpen(true);
|
||||
}, [
|
||||
agent,
|
||||
graph,
|
||||
allRequiredInputsAreSet,
|
||||
allCredentialsAreSet,
|
||||
notifyMissingInputs,
|
||||
]);
|
||||
|
||||
const doSetupSchedule = useCallback(
|
||||
(cronExpression: string, scheduleName: string) => {
|
||||
async (cronExpression: string, scheduleName: string) => {
|
||||
// Scheduling is not supported for webhook-triggered agents
|
||||
if (agent.has_external_trigger) return;
|
||||
if (graph.has_external_trigger) return;
|
||||
|
||||
api
|
||||
if (_doCreateSchedule) {
|
||||
await _doCreateSchedule(
|
||||
cronExpression,
|
||||
scheduleName || graph.name,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const schedule = await api
|
||||
.createGraphExecutionSchedule({
|
||||
graph_id: agent.graph_id,
|
||||
graph_version: agent.graph_version,
|
||||
name: scheduleName || agent.name,
|
||||
graph_id: graph.id,
|
||||
graph_version: graph.version,
|
||||
name: scheduleName || graph.name,
|
||||
cron: cronExpression,
|
||||
inputs: inputValues,
|
||||
credentials: inputCredentials,
|
||||
})
|
||||
.then((schedule) => onCreateSchedule(schedule))
|
||||
.catch(toastOnFail("set up agent run schedule"));
|
||||
|
||||
if (schedule && onCreateSchedule) onCreateSchedule(schedule);
|
||||
},
|
||||
[api, agent, inputValues, inputCredentials, onCreateSchedule, toastOnFail],
|
||||
[api, graph, inputValues, inputCredentials, onCreateSchedule, toastOnFail],
|
||||
);
|
||||
|
||||
const runActions: ButtonAction[] = useMemo(
|
||||
() => [
|
||||
// "Regular" agent: [run] + [save as preset] buttons
|
||||
...(!agent.has_external_trigger
|
||||
...(!graph.has_external_trigger
|
||||
? ([
|
||||
{
|
||||
label: (
|
||||
@@ -378,6 +394,7 @@ export default function AgentRunDraftView({
|
||||
),
|
||||
variant: "accent",
|
||||
callback: doRun,
|
||||
extraProps: { "data-testid": "agent-run-button" },
|
||||
},
|
||||
{
|
||||
label: (
|
||||
@@ -403,7 +420,7 @@ export default function AgentRunDraftView({
|
||||
] satisfies ButtonAction[])
|
||||
: []),
|
||||
// Triggered agent: [setup] button
|
||||
...(agent.has_external_trigger && !agentPreset?.webhook_id
|
||||
...(graph.has_external_trigger && !agentPreset?.webhook_id
|
||||
? ([
|
||||
{
|
||||
label: (
|
||||
@@ -466,7 +483,7 @@ export default function AgentRunDraftView({
|
||||
label: (
|
||||
<>
|
||||
<Trash2Icon className="mr-2 size-4" />
|
||||
Delete {agent.has_external_trigger ? "trigger" : "preset"}
|
||||
Delete {graph.has_external_trigger ? "trigger" : "preset"}
|
||||
</>
|
||||
),
|
||||
callback: () => doDeletePreset(agentPreset.id),
|
||||
@@ -475,7 +492,7 @@ export default function AgentRunDraftView({
|
||||
: []),
|
||||
],
|
||||
[
|
||||
agent.has_external_trigger,
|
||||
graph.has_external_trigger,
|
||||
agentPreset,
|
||||
doRun,
|
||||
doSetupTrigger,
|
||||
@@ -491,26 +508,26 @@ export default function AgentRunDraftView({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="agpt-div flex gap-6">
|
||||
<div className={cn("agpt-div flex gap-6", className)}>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<Card className="agpt-box">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-lg">Input</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{(agentPreset || agent.has_external_trigger) && (
|
||||
{(agentPreset || graph.has_external_trigger) && (
|
||||
<>
|
||||
{/* Preset name and description */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
{agent.has_external_trigger ? "Trigger" : "Preset"} Name
|
||||
{graph.has_external_trigger ? "Trigger" : "Preset"} Name
|
||||
<SchemaTooltip
|
||||
description={`Name of the ${agent.has_external_trigger ? "trigger" : "preset"} you are setting up`}
|
||||
description={`Name of the ${graph.has_external_trigger ? "trigger" : "preset"} you are setting up`}
|
||||
/>
|
||||
</label>
|
||||
<Input
|
||||
value={presetName}
|
||||
placeholder={`Enter ${agent.has_external_trigger ? "trigger" : "preset"} name`}
|
||||
placeholder={`Enter ${graph.has_external_trigger ? "trigger" : "preset"} name`}
|
||||
onChange={(e) => {
|
||||
setPresetName(e.target.value);
|
||||
setChangedPresetAttributes((prev) => prev.add("name"));
|
||||
@@ -519,15 +536,15 @@ export default function AgentRunDraftView({
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
{agent.has_external_trigger ? "Trigger" : "Preset"}{" "}
|
||||
{graph.has_external_trigger ? "Trigger" : "Preset"}{" "}
|
||||
Description
|
||||
<SchemaTooltip
|
||||
description={`Description of the ${agent.has_external_trigger ? "trigger" : "preset"} you are setting up`}
|
||||
description={`Description of the ${graph.has_external_trigger ? "trigger" : "preset"} you are setting up`}
|
||||
/>
|
||||
</label>
|
||||
<Input
|
||||
value={presetDescription}
|
||||
placeholder={`Enter ${agent.has_external_trigger ? "trigger" : "preset"} description`}
|
||||
placeholder={`Enter ${graph.has_external_trigger ? "trigger" : "preset"} description`}
|
||||
onChange={(e) => {
|
||||
setPresetDescription(e.target.value);
|
||||
setChangedPresetAttributes((prev) =>
|
||||
@@ -565,7 +582,7 @@ export default function AgentRunDraftView({
|
||||
);
|
||||
}}
|
||||
hideIfSingleCredentialAvailable={
|
||||
!agentPreset && !agent.has_external_trigger
|
||||
!agentPreset && !graph.has_external_trigger
|
||||
}
|
||||
/>
|
||||
),
|
||||
@@ -590,6 +607,7 @@ export default function AgentRunDraftView({
|
||||
}));
|
||||
setChangedPresetAttributes((prev) => prev.add("inputs"));
|
||||
}}
|
||||
data-testid={`agent-input-${key}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -601,17 +619,19 @@ export default function AgentRunDraftView({
|
||||
<aside className="w-48 xl:w-56">
|
||||
<div className="flex flex-col gap-8">
|
||||
<ActionButtonGroup
|
||||
title={`${agent.has_external_trigger ? "Trigger" : agentPreset ? "Preset" : "Run"} actions`}
|
||||
title={`${graph.has_external_trigger ? "Trigger" : agentPreset ? "Preset" : "Run"} actions`}
|
||||
actions={runActions}
|
||||
/>
|
||||
<CronSchedulerDialog
|
||||
open={cronScheduleDialogOpen}
|
||||
setOpen={setCronScheduleDialogOpen}
|
||||
afterCronCreation={doSetupSchedule}
|
||||
defaultScheduleName={agent.name}
|
||||
defaultScheduleName={graph.name}
|
||||
/>
|
||||
|
||||
<ActionButtonGroup title="Agent actions" actions={agentActions} />
|
||||
{agentActions && agentActions.length > 0 && (
|
||||
<ActionButtonGroup title="Agent actions" actions={agentActions} />
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function ActionButtonGroup({
|
||||
variant={action.variant ?? "outline"}
|
||||
disabled={action.disabled}
|
||||
onClick={action.callback}
|
||||
{...action.extraProps}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
@@ -36,6 +37,7 @@ export default function ActionButtonGroup({
|
||||
"pointer-events-none border-zinc-400 text-zinc-400",
|
||||
)}
|
||||
href={action.href}
|
||||
{...action.extraProps}
|
||||
>
|
||||
{action.label}
|
||||
</Link>
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import type { ButtonProps } from "@/components/agptui/Button";
|
||||
import type { LinkProps } from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export type ButtonAction = {
|
||||
label: React.ReactNode;
|
||||
variant?: ButtonProps["variant"];
|
||||
disabled?: boolean;
|
||||
} & ({ callback: () => void } | { href: string });
|
||||
extraProps?: Record<`data-${string}`, string>;
|
||||
} & (
|
||||
| {
|
||||
callback: () => void;
|
||||
extraProps?: Partial<ButtonProps>;
|
||||
}
|
||||
| {
|
||||
href: string;
|
||||
extraProps?: Partial<LinkProps>;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import React, { useCallback, useMemo, useState, useDeferredValue } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -24,8 +24,36 @@ import {
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import jaro from "jaro-winkler";
|
||||
|
||||
type _Block = Block & {
|
||||
uiKey?: string;
|
||||
hardcodedValues?: Record<string, any>;
|
||||
_cached?: {
|
||||
blockName: string;
|
||||
beautifiedName: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Hook to preprocess blocks with cached expensive operations
|
||||
const useSearchableBlocks = (blocks: _Block[]): _Block[] => {
|
||||
return useMemo(
|
||||
() =>
|
||||
blocks.map((block) => {
|
||||
if (!block._cached) {
|
||||
block._cached = {
|
||||
blockName: block.name.toLowerCase(),
|
||||
beautifiedName: beautifyString(block.name).toLowerCase(),
|
||||
description: block.description.toLowerCase(),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
}),
|
||||
[blocks],
|
||||
);
|
||||
};
|
||||
|
||||
interface BlocksControlProps {
|
||||
blocks: Block[];
|
||||
blocks: _Block[];
|
||||
addBlock: (
|
||||
id: string,
|
||||
name: string,
|
||||
@@ -45,16 +73,19 @@ interface BlocksControlProps {
|
||||
* @param {(id: string, name: string) => void} BlocksControlProps.addBlock - A function to call when a block is added.
|
||||
* @returns The rendered BlocksControl component.
|
||||
*/
|
||||
export const BlocksControl: React.FC<BlocksControlProps> = ({
|
||||
blocks,
|
||||
export function BlocksControl({
|
||||
blocks: _blocks,
|
||||
addBlock,
|
||||
pinBlocksPopover,
|
||||
flows,
|
||||
nodes,
|
||||
}) => {
|
||||
}: BlocksControlProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const deferredSearchQuery = useDeferredValue(searchQuery);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
|
||||
const blocks = useSearchableBlocks(_blocks);
|
||||
|
||||
const graphHasWebhookNodes = nodes.some((n) =>
|
||||
[BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(n.data.uiType),
|
||||
);
|
||||
@@ -66,9 +97,10 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
|
||||
const blockList = blocks
|
||||
.filter((b) => b.uiType !== BlockUIType.AGENT)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const agentBlockList = flows.map(
|
||||
(flow) =>
|
||||
({
|
||||
|
||||
const agentBlockList = flows
|
||||
.map(
|
||||
(flow): _Block => ({
|
||||
id: SpecialBlockID.AGENT,
|
||||
name: flow.name,
|
||||
description:
|
||||
@@ -79,75 +111,32 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
|
||||
outputSchema: flow.output_schema,
|
||||
staticOutput: false,
|
||||
uiType: BlockUIType.AGENT,
|
||||
uiKey: flow.id,
|
||||
costs: [],
|
||||
uiKey: flow.id,
|
||||
hardcodedValues: {
|
||||
graph_id: flow.id,
|
||||
graph_version: flow.version,
|
||||
input_schema: flow.input_schema,
|
||||
output_schema: flow.output_schema,
|
||||
},
|
||||
}) satisfies Block,
|
||||
);
|
||||
|
||||
/**
|
||||
* Evaluates how well a block matches the search query and returns a relevance score.
|
||||
* The scoring algorithm works as follows:
|
||||
* - Returns 1 if no query (all blocks match equally)
|
||||
* - Normalized query for case-insensitive matching
|
||||
* - Returns 3 for exact substring matches in block name (highest priority)
|
||||
* - Returns 2 when all query words appear in the block name (regardless of order)
|
||||
* - Returns 1.X for blocks with names similar to query using Jaro-Winkler distance (X is similarity score)
|
||||
* - Returns 0.5 when all query words appear in the block description (lowest priority)
|
||||
* - Returns 0 for no match
|
||||
*
|
||||
* Higher scores will appear first in search results.
|
||||
*/
|
||||
const matchesSearch = (block: Block, query: string): number => {
|
||||
if (!query) return 1;
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
const queryWords = normalizedQuery.split(/\s+/);
|
||||
const blockName = block.name.toLowerCase();
|
||||
const beautifiedName = beautifyString(block.name).toLowerCase();
|
||||
const description = block.description.toLowerCase();
|
||||
|
||||
// 1. Exact match in name (highest priority)
|
||||
if (
|
||||
blockName.includes(normalizedQuery) ||
|
||||
beautifiedName.includes(normalizedQuery)
|
||||
) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// 2. All query words in name (regardless of order)
|
||||
const allWordsInName = queryWords.every(
|
||||
(word) => blockName.includes(word) || beautifiedName.includes(word),
|
||||
}),
|
||||
)
|
||||
.map(
|
||||
(agentBlock): _Block => ({
|
||||
...agentBlock,
|
||||
_cached: {
|
||||
blockName: agentBlock.name.toLowerCase(),
|
||||
beautifiedName: beautifyString(agentBlock.name).toLowerCase(),
|
||||
description: agentBlock.description.toLowerCase(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (allWordsInName) return 2;
|
||||
|
||||
// 3. Similarity with name (Jaro-Winkler)
|
||||
const similarityThreshold = 0.65;
|
||||
const nameSimilarity = jaro(blockName, normalizedQuery);
|
||||
const beautifiedSimilarity = jaro(beautifiedName, normalizedQuery);
|
||||
const maxSimilarity = Math.max(nameSimilarity, beautifiedSimilarity);
|
||||
if (maxSimilarity > similarityThreshold) {
|
||||
return 1 + maxSimilarity; // Score between 1 and 2
|
||||
}
|
||||
|
||||
// 4. All query words in description (lower priority)
|
||||
const allWordsInDescription = queryWords.every((word) =>
|
||||
description.includes(word),
|
||||
);
|
||||
if (allWordsInDescription) return 0.5;
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
return blockList
|
||||
.concat(agentBlockList)
|
||||
.map((block) => ({
|
||||
block,
|
||||
score: matchesSearch(block, searchQuery),
|
||||
score: blockScoreForQuery(block, deferredSearchQuery),
|
||||
}))
|
||||
.filter(
|
||||
({ block, score }) =>
|
||||
@@ -173,26 +162,28 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
|
||||
}, [
|
||||
blocks,
|
||||
flows,
|
||||
searchQuery,
|
||||
selectedCategory,
|
||||
deferredSearchQuery,
|
||||
graphHasInputNodes,
|
||||
graphHasWebhookNodes,
|
||||
]);
|
||||
|
||||
const resetFilters = React.useCallback(() => {
|
||||
const resetFilters = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
setSelectedCategory(null);
|
||||
}, []);
|
||||
|
||||
// Extract unique categories from blocks
|
||||
const categories = Array.from(
|
||||
new Set([
|
||||
null,
|
||||
...blocks
|
||||
.flatMap((block) => block.categories.map((cat) => cat.category))
|
||||
.sort(),
|
||||
]),
|
||||
);
|
||||
const categories = useMemo(() => {
|
||||
return Array.from(
|
||||
new Set([
|
||||
null,
|
||||
...blocks
|
||||
.flatMap((block) => block.categories.map((cat) => cat.category))
|
||||
.sort(),
|
||||
]),
|
||||
);
|
||||
}, [blocks]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
@@ -336,4 +327,57 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates how well a block matches the search query and returns a relevance score.
|
||||
* The scoring algorithm works as follows:
|
||||
* - Returns 1 if no query (all blocks match equally)
|
||||
* - Normalized query for case-insensitive matching
|
||||
* - Returns 3 for exact substring matches in block name (highest priority)
|
||||
* - Returns 2 when all query words appear in the block name (regardless of order)
|
||||
* - Returns 1.X for blocks with names similar to query using Jaro-Winkler distance (X is similarity score)
|
||||
* - Returns 0.5 when all query words appear in the block description (lowest priority)
|
||||
* - Returns 0 for no match
|
||||
*
|
||||
* Higher scores will appear first in search results.
|
||||
*/
|
||||
function blockScoreForQuery(block: _Block, query: string): number {
|
||||
if (!query) return 1;
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
const queryWords = normalizedQuery.split(/\s+/);
|
||||
|
||||
// Use cached values for performance
|
||||
const { blockName, beautifiedName, description } = block._cached!;
|
||||
|
||||
// 1. Exact match in name (highest priority)
|
||||
if (
|
||||
blockName.includes(normalizedQuery) ||
|
||||
beautifiedName.includes(normalizedQuery)
|
||||
) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// 2. All query words in name (regardless of order)
|
||||
const allWordsInName = queryWords.every(
|
||||
(word) => blockName.includes(word) || beautifiedName.includes(word),
|
||||
);
|
||||
if (allWordsInName) return 2;
|
||||
|
||||
// 3. Similarity with name (Jaro-Winkler)
|
||||
const similarityThreshold = 0.65;
|
||||
const nameSimilarity = jaro(blockName, normalizedQuery);
|
||||
const beautifiedSimilarity = jaro(beautifiedName, normalizedQuery);
|
||||
const maxSimilarity = Math.max(nameSimilarity, beautifiedSimilarity);
|
||||
if (maxSimilarity > similarityThreshold) {
|
||||
return 1 + maxSimilarity; // Score between 1 and 2
|
||||
}
|
||||
|
||||
// 4. All query words in description (lower priority)
|
||||
const allWordsInDescription = queryWords.every((word) =>
|
||||
description.includes(word),
|
||||
);
|
||||
if (allWordsInDescription) return 0.5;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -22,7 +22,7 @@ interface SaveControlProps {
|
||||
agentName: string;
|
||||
agentDescription: string;
|
||||
canSave: boolean;
|
||||
onSave: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
onNameChange: (name: string) => void;
|
||||
onDescriptionChange: (description: string) => void;
|
||||
pinSavePopover: boolean;
|
||||
@@ -56,17 +56,13 @@ export const SaveControl = ({
|
||||
* We should migrate this to be handled with form controls and a form library.
|
||||
*/
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave();
|
||||
}, [onSave]);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault(); // Stop the browser default action
|
||||
handleSave(); // Call your save function
|
||||
await onSave(); // Call your save function
|
||||
toast({
|
||||
duration: 2000,
|
||||
title: "All changes saved successfully!",
|
||||
@@ -79,7 +75,7 @@ export const SaveControl = ({
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleSave, toast]);
|
||||
}, [onSave, toast]);
|
||||
|
||||
return (
|
||||
<Popover open={pinSavePopover ? true : undefined}>
|
||||
@@ -154,7 +150,7 @@ export const SaveControl = ({
|
||||
<CardFooter className="flex flex-col items-stretch gap-2">
|
||||
<Button
|
||||
className="w-full dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-800"
|
||||
onClick={handleSave}
|
||||
onClick={onSave}
|
||||
data-id="save-control-save-agent"
|
||||
data-testid="save-control-save-agent-button"
|
||||
disabled={!canSave}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
GraphExecutionMeta,
|
||||
Graph,
|
||||
BlockUIType,
|
||||
BlockIORootSchema,
|
||||
GraphExecutionMeta,
|
||||
LibraryAgent,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -25,7 +23,7 @@ import {
|
||||
TrashIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { exportAsJSONFile, filterBlocksByType } from "@/lib/utils";
|
||||
import { exportAsJSONFile } from "@/lib/utils";
|
||||
import { FlowRunsStats } from "@/components/monitor/index";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -35,9 +33,9 @@ import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import RunnerInputUI from "@/components/runner-ui/RunnerInputUI";
|
||||
import useAgentGraph from "@/hooks/useAgentGraph";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { RunnerInputDialog } from "@/components/runner-ui/RunnerInputUI";
|
||||
|
||||
export const FlowInfo: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
@@ -47,8 +45,12 @@ export const FlowInfo: React.FC<
|
||||
refresh: () => void;
|
||||
}
|
||||
> = ({ flow, executions, flowVersion, refresh, ...props }) => {
|
||||
const { requestSaveAndRun, requestStopRun, isRunning, nodes, setNodes } =
|
||||
useAgentGraph(flow.graph_id, flow.graph_version, undefined, false);
|
||||
const { savedAgent, saveAndRun, stopRun, isRunning } = useAgentGraph(
|
||||
flow.graph_id,
|
||||
flow.graph_version,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
const api = useBackendAPI();
|
||||
|
||||
@@ -62,90 +64,30 @@ export const FlowInfo: React.FC<
|
||||
(selectedVersion == "all" ? flow.graph_version : selectedVersion),
|
||||
);
|
||||
|
||||
const hasInputs = Object.keys(flow.input_schema.properties).length > 0;
|
||||
const hasCredentialsInputs =
|
||||
Object.keys(flow.credentials_input_schema.properties).length > 0;
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isRunnerInputOpen, setIsRunnerInputOpen] = useState(false);
|
||||
const [isRunDialogOpen, setIsRunDialogOpen] = useState(false);
|
||||
const isDisabled = !selectedFlowVersion;
|
||||
|
||||
const getBlockInputsAndOutputs = useCallback(() => {
|
||||
const inputBlocks = filterBlocksByType(
|
||||
nodes,
|
||||
(node) => node.data.uiType === BlockUIType.INPUT,
|
||||
);
|
||||
|
||||
const outputBlocks = filterBlocksByType(
|
||||
nodes,
|
||||
(node) => node.data.uiType === BlockUIType.OUTPUT,
|
||||
);
|
||||
|
||||
const inputs = inputBlocks.map((node) => ({
|
||||
id: node.id,
|
||||
type: "input" as const,
|
||||
inputSchema: node.data.inputSchema as BlockIORootSchema,
|
||||
hardcodedValues: {
|
||||
name: (node.data.hardcodedValues as any).name || "",
|
||||
description: (node.data.hardcodedValues as any).description || "",
|
||||
value: (node.data.hardcodedValues as any).value,
|
||||
placeholder_values:
|
||||
(node.data.hardcodedValues as any).placeholder_values || [],
|
||||
},
|
||||
}));
|
||||
|
||||
const outputs = outputBlocks.map((node) => ({
|
||||
id: node.id,
|
||||
type: "output" as const,
|
||||
hardcodedValues: {
|
||||
name: (node.data.hardcodedValues as any).name || "Output",
|
||||
description:
|
||||
(node.data.hardcodedValues as any).description ||
|
||||
"Output from the agent",
|
||||
value: (node.data.hardcodedValues as any).value,
|
||||
},
|
||||
result: (node.data.executionResults as any)?.at(-1)?.data?.output,
|
||||
}));
|
||||
|
||||
return { inputs, outputs };
|
||||
}, [nodes]);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.getGraphAllVersions(flow.graph_id)
|
||||
.then((result) => setFlowVersions(result));
|
||||
}, [flow.graph_id, api]);
|
||||
|
||||
const openRunnerInput = () => setIsRunnerInputOpen(true);
|
||||
const openRunDialog = () => setIsRunDialogOpen(true);
|
||||
|
||||
const runOrOpenInput = () => {
|
||||
const { inputs } = getBlockInputsAndOutputs();
|
||||
if (inputs.length > 0) {
|
||||
openRunnerInput();
|
||||
if (hasInputs || hasCredentialsInputs) {
|
||||
openRunDialog();
|
||||
} else {
|
||||
requestSaveAndRun();
|
||||
saveAndRun({}, {});
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(nodeId: string, field: string, value: any) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
hardcodedValues: {
|
||||
...(node.data.hardcodedValues as any),
|
||||
[field]: value,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setNodes],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader className="">
|
||||
@@ -222,7 +164,7 @@ export const FlowInfo: React.FC<
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-purple-500 text-white hover:bg-purple-700"
|
||||
onClick={isRunning ? requestStopRun : runOrOpenInput}
|
||||
onClick={!isRunning ? runOrOpenInput : stopRun}
|
||||
disabled={isDisabled}
|
||||
title={!isRunning ? "Run Agent" : "Stop Agent"}
|
||||
>
|
||||
@@ -282,20 +224,14 @@ export const FlowInfo: React.FC<
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<RunnerInputUI
|
||||
isOpen={isRunnerInputOpen}
|
||||
onClose={() => setIsRunnerInputOpen(false)}
|
||||
blockInputs={getBlockInputsAndOutputs().inputs}
|
||||
onInputChange={handleInputChange}
|
||||
onRun={() => {
|
||||
setIsRunnerInputOpen(false);
|
||||
requestSaveAndRun();
|
||||
}}
|
||||
isRunning={isRunning}
|
||||
scheduledInput={false}
|
||||
isScheduling={false}
|
||||
onSchedule={async () => {}} // Fixed type error by making async
|
||||
/>
|
||||
{savedAgent && (
|
||||
<RunnerInputDialog
|
||||
isOpen={isRunDialogOpen}
|
||||
doClose={() => setIsRunDialogOpen(false)}
|
||||
graph={savedAgent}
|
||||
doRun={saveAndRun}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IconSquare } from "@/components/ui/icons";
|
||||
import { ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
|
||||
import moment from "moment/moment";
|
||||
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
|
||||
import RunnerOutputUI, { BlockOutput } from "../runner-ui/RunnerOutputUI";
|
||||
import RunnerOutputUI, { OutputNodeInfo } from "../runner-ui/RunnerOutputUI";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
export const FlowRunInfo: React.FC<
|
||||
@@ -17,7 +17,7 @@ export const FlowRunInfo: React.FC<
|
||||
}
|
||||
> = ({ agent, execution, ...props }) => {
|
||||
const [isOutputOpen, setIsOutputOpen] = useState(false);
|
||||
const [blockOutputs, setBlockOutputs] = useState<BlockOutput[]>([]);
|
||||
const [blockOutputs, setBlockOutputs] = useState<OutputNodeInfo[]>([]);
|
||||
const api = useBackendAPI();
|
||||
|
||||
const fetchBlockResults = useCallback(async () => {
|
||||
@@ -40,7 +40,7 @@ export const FlowRunInfo: React.FC<
|
||||
"Output from the agent",
|
||||
},
|
||||
result: value,
|
||||
}) satisfies BlockOutput,
|
||||
}) satisfies OutputNodeInfo,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -121,8 +121,8 @@ export const FlowRunInfo: React.FC<
|
||||
</Card>
|
||||
<RunnerOutputUI
|
||||
isOpen={isOutputOpen}
|
||||
onClose={() => setIsOutputOpen(false)}
|
||||
blockOutputs={blockOutputs}
|
||||
doClose={() => setIsOutputOpen(false)}
|
||||
outputs={blockOutputs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
BlockIOStringSubSchema,
|
||||
BlockIOSubSchema,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { TypeBasedInput } from "@/components/type-based-input";
|
||||
import SchemaTooltip from "../SchemaTooltip";
|
||||
|
||||
interface InputBlockProps {
|
||||
id: string;
|
||||
name: string;
|
||||
schema: BlockIOSubSchema;
|
||||
description?: string;
|
||||
value: string;
|
||||
placeholder_values?: any[];
|
||||
onInputChange: (id: string, field: string, value: string) => void;
|
||||
}
|
||||
|
||||
export function InputBlock({
|
||||
id,
|
||||
name,
|
||||
schema,
|
||||
description,
|
||||
value,
|
||||
placeholder_values,
|
||||
onInputChange,
|
||||
}: InputBlockProps) {
|
||||
if (placeholder_values && placeholder_values.length > 0) {
|
||||
schema = { ...schema, enum: placeholder_values } as BlockIOStringSubSchema;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
{name || "Unnamed Input"}
|
||||
<SchemaTooltip description={description} />
|
||||
</label>
|
||||
<TypeBasedInput
|
||||
id={`${id}-Value`}
|
||||
data-testid={`run-dialog-input-${name}`}
|
||||
schema={schema}
|
||||
value={value}
|
||||
placeholder={description}
|
||||
onChange={(value) => onInputChange(id, "value", value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { InputBlock } from "./RunnerInputBlock";
|
||||
import { BlockInput } from "./RunnerInputUI";
|
||||
|
||||
interface InputListProps {
|
||||
blockInputs: BlockInput[];
|
||||
onInputChange: (nodeId: string, field: string, value: any) => void;
|
||||
}
|
||||
|
||||
export function InputList({ blockInputs, onInputChange }: InputListProps) {
|
||||
return (
|
||||
<ScrollArea className="max-h-[60vh] overflow-auto">
|
||||
<div className="space-y-4">
|
||||
{blockInputs && blockInputs.length > 0 ? (
|
||||
blockInputs.map((block) => (
|
||||
<InputBlock
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
schema={block.inputSchema}
|
||||
name={block.hardcodedValues.name}
|
||||
description={block.hardcodedValues.description}
|
||||
value={block.hardcodedValues.value ?? ""}
|
||||
placeholder_values={block.hardcodedValues.placeholder_values}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p>No input blocks available.</p>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +1,90 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import type {
|
||||
CredentialsMetaInput,
|
||||
GraphMeta,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BlockIORootSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { InputList } from "./RunnerInputList";
|
||||
import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
|
||||
|
||||
export interface BlockInput {
|
||||
id: string;
|
||||
inputSchema: BlockIORootSchema;
|
||||
hardcodedValues: {
|
||||
name: string;
|
||||
description: string;
|
||||
value: any;
|
||||
placeholder_values?: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface RunSettingsUiProps {
|
||||
interface RunInputDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
blockInputs: BlockInput[];
|
||||
onInputChange: (nodeId: string, field: string, value: any) => void;
|
||||
onRun: () => void;
|
||||
onSchedule: () => Promise<void>;
|
||||
scheduledInput: boolean;
|
||||
isScheduling: boolean;
|
||||
isRunning: boolean;
|
||||
doClose: () => void;
|
||||
graph: GraphMeta;
|
||||
doRun?: (
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => Promise<void> | void;
|
||||
doCreateSchedule?: (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function RunnerInputUI({
|
||||
export function RunnerInputDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
blockInputs,
|
||||
isScheduling,
|
||||
onInputChange,
|
||||
onRun,
|
||||
onSchedule,
|
||||
scheduledInput,
|
||||
isRunning,
|
||||
}: RunSettingsUiProps) {
|
||||
const handleRun = () => {
|
||||
onRun();
|
||||
onClose();
|
||||
};
|
||||
doClose,
|
||||
graph,
|
||||
doRun,
|
||||
doCreateSchedule,
|
||||
}: RunInputDialogProps) {
|
||||
const handleRun = useCallback(
|
||||
doRun
|
||||
? async (
|
||||
inputs: Record<string, any>,
|
||||
credentials_inputs: Record<string, CredentialsMetaInput>,
|
||||
) => {
|
||||
await doRun(inputs, credentials_inputs);
|
||||
doClose();
|
||||
}
|
||||
: async () => {},
|
||||
[doRun, doClose],
|
||||
);
|
||||
|
||||
const handleSchedule = async () => {
|
||||
onClose();
|
||||
await onSchedule();
|
||||
};
|
||||
const handleSchedule = useCallback(
|
||||
doCreateSchedule
|
||||
? async (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
inputs: Record<string, any>,
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => {
|
||||
await doCreateSchedule(
|
||||
cronExpression,
|
||||
scheduleName,
|
||||
inputs,
|
||||
credentialsInputs,
|
||||
);
|
||||
doClose();
|
||||
}
|
||||
: async () => {},
|
||||
[doCreateSchedule, doClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="flex flex-col px-10 py-8">
|
||||
<Dialog open={isOpen} onOpenChange={doClose}>
|
||||
<DialogContent className="flex w-[90vw] max-w-4xl flex-col p-10">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
{scheduledInput ? "Schedule Settings" : "Run Settings"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 text-sm">
|
||||
Configure settings for running your agent.
|
||||
</DialogDescription>
|
||||
<DialogTitle className="text-2xl">Run your agent</DialogTitle>
|
||||
<DialogDescription className="mt-2">{graph.name}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<InputList blockInputs={blockInputs} onInputChange={onInputChange} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
data-testid="run-dialog-run-button"
|
||||
onClick={scheduledInput ? handleSchedule : handleRun}
|
||||
className="text-lg"
|
||||
disabled={scheduledInput ? isScheduling : isRunning}
|
||||
>
|
||||
{scheduledInput ? "Schedule" : isRunning ? "Running..." : "Run"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<AgentRunDraftView
|
||||
className="p-0"
|
||||
graph={graph}
|
||||
doRun={doRun ? handleRun : undefined}
|
||||
onRun={doRun ? undefined : doClose}
|
||||
doCreateSchedule={doCreateSchedule ? handleSchedule : undefined}
|
||||
onCreateSchedule={doCreateSchedule ? undefined : doClose}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default RunnerInputUI;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Clipboard } from "lucide-react";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
|
||||
export type BlockOutput = {
|
||||
export type OutputNodeInfo = {
|
||||
metadata: {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -23,8 +23,8 @@ export type BlockOutput = {
|
||||
|
||||
interface OutputModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
blockOutputs: BlockOutput[];
|
||||
doClose: () => void;
|
||||
outputs: OutputNodeInfo[];
|
||||
}
|
||||
|
||||
const formatOutput = (output: any): string => {
|
||||
@@ -47,11 +47,7 @@ const formatOutput = (output: any): string => {
|
||||
return String(output);
|
||||
};
|
||||
|
||||
export function RunnerOutputUI({
|
||||
isOpen,
|
||||
onClose,
|
||||
blockOutputs,
|
||||
}: OutputModalProps) {
|
||||
export function RunnerOutputUI({ isOpen, doClose, outputs }: OutputModalProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const copyOutput = (name: string, output: any) => {
|
||||
@@ -70,7 +66,7 @@ export function RunnerOutputUI({
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={onClose}>
|
||||
<Sheet open={isOpen} onOpenChange={doClose}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="flex h-full w-full flex-col overflow-hidden sm:max-w-[600px]"
|
||||
@@ -84,16 +80,16 @@ export function RunnerOutputUI({
|
||||
<div className="flex-grow overflow-y-auto px-2 py-2">
|
||||
<ScrollArea className="h-full overflow-auto pr-4">
|
||||
<div className="space-y-4">
|
||||
{blockOutputs && blockOutputs.length > 0 ? (
|
||||
blockOutputs.map((block, i) => (
|
||||
{outputs && outputs.length > 0 ? (
|
||||
outputs.map((output, i) => (
|
||||
<div key={i} className="space-y-1">
|
||||
<Label className="text-base font-semibold">
|
||||
{block.metadata.name || "Unnamed Output"}
|
||||
{output.metadata.name || "Unnamed Output"}
|
||||
</Label>
|
||||
|
||||
{block.metadata.description && (
|
||||
{output.metadata.description && (
|
||||
<Label className="block text-sm text-gray-600">
|
||||
{block.metadata.description}
|
||||
{output.metadata.description}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
@@ -104,8 +100,8 @@ export function RunnerOutputUI({
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
copyOutput(
|
||||
block.metadata.name || "Unnamed Output",
|
||||
block.result,
|
||||
output.metadata.name || "Unnamed Output",
|
||||
output.result,
|
||||
)
|
||||
}
|
||||
title="Copy Output"
|
||||
@@ -114,7 +110,7 @@ export function RunnerOutputUI({
|
||||
</Button>
|
||||
<Textarea
|
||||
readOnly
|
||||
value={formatOutput(block.result ?? "No output yet")}
|
||||
value={formatOutput(output.result ?? "No output yet")}
|
||||
className="w-full resize-none whitespace-pre-wrap break-words border-none bg-transparent text-sm"
|
||||
style={{
|
||||
height: "auto",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { Node, Edge, useReactFlow, useViewport } from "@xyflow/react";
|
||||
import { Node, Edge, useReactFlow } from "@xyflow/react";
|
||||
|
||||
interface CopyableData {
|
||||
nodes: Node[];
|
||||
@@ -7,8 +7,9 @@ interface CopyableData {
|
||||
}
|
||||
|
||||
export function useCopyPaste(getNextNodeId: () => string) {
|
||||
const { setNodes, addEdges, getNodes, getEdges } = useReactFlow();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const { setNodes, addEdges, getNodes, getEdges, getViewport } =
|
||||
useReactFlow();
|
||||
const { x, y, zoom } = getViewport();
|
||||
|
||||
const handleCopyPaste = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
|
||||
@@ -471,7 +471,7 @@ export default class BackendAPI {
|
||||
);
|
||||
}
|
||||
|
||||
getAgentMetaByStoreListingVersionId(
|
||||
getGraphMetaByStoreListingVersionID(
|
||||
storeListingVersionID: string,
|
||||
): Promise<GraphMeta> {
|
||||
return this._get(`/store/graph/${storeListingVersionID}`);
|
||||
@@ -672,21 +672,16 @@ export default class BackendAPI {
|
||||
return this._request("POST", `/library/agents/${libraryAgentId}/fork`);
|
||||
}
|
||||
|
||||
async setupAgentTrigger(
|
||||
libraryAgentID: LibraryAgentID,
|
||||
params: {
|
||||
name: string;
|
||||
description?: string;
|
||||
trigger_config: Record<string, any>;
|
||||
agent_credentials: Record<string, CredentialsMetaInput>;
|
||||
},
|
||||
): Promise<LibraryAgentPreset> {
|
||||
async setupAgentTrigger(params: {
|
||||
name: string;
|
||||
description?: string;
|
||||
graph_id: GraphID;
|
||||
graph_version: number;
|
||||
trigger_config: Record<string, any>;
|
||||
agent_credentials: Record<string, CredentialsMetaInput>;
|
||||
}): Promise<LibraryAgentPreset> {
|
||||
return parseLibraryAgentPresetTimestamp(
|
||||
await this._request(
|
||||
"POST",
|
||||
`/library/agents/${libraryAgentID}/setup-trigger`,
|
||||
params,
|
||||
),
|
||||
await this._request("POST", `/library/presets/setup-trigger`, params),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export enum BlockCostType {
|
||||
export type BlockCost = {
|
||||
cost_amount: number;
|
||||
cost_type: BlockCostType;
|
||||
cost_filter: { [key: string]: any };
|
||||
cost_filter: Record<string, any>;
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/block.py:Block */
|
||||
@@ -37,14 +37,12 @@ export type Block = {
|
||||
outputSchema: BlockIORootSchema;
|
||||
staticOutput: boolean;
|
||||
uiType: BlockUIType;
|
||||
uiKey?: string;
|
||||
costs: BlockCost[];
|
||||
hardcodedValues: { [key: string]: any } | null;
|
||||
};
|
||||
|
||||
export type BlockIORootSchema = {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOSubSchema };
|
||||
properties: Record<string, BlockIOSubSchema>;
|
||||
required?: (keyof BlockIORootSchema["properties"])[];
|
||||
additionalProperties?: { type: string };
|
||||
};
|
||||
@@ -93,9 +91,9 @@ export type BlockIOSubSchemaMeta = {
|
||||
|
||||
export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOSubSchema };
|
||||
const?: { [key: keyof BlockIOObjectSubSchema["properties"]]: any };
|
||||
default?: { [key: keyof BlockIOObjectSubSchema["properties"]]: any };
|
||||
properties: Record<string, BlockIOSubSchema>;
|
||||
const?: Record<keyof BlockIOObjectSubSchema["properties"], any>;
|
||||
default?: Record<keyof BlockIOObjectSubSchema["properties"], any>;
|
||||
required?: (keyof BlockIOObjectSubSchema["properties"])[];
|
||||
secret?: boolean;
|
||||
};
|
||||
@@ -103,8 +101,8 @@ export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
|
||||
export type BlockIOKVSubSchema = BlockIOSubSchemaMeta & {
|
||||
type: "object";
|
||||
additionalProperties?: { type: "string" | "number" | "integer" };
|
||||
const?: { [key: string]: string | number };
|
||||
default?: { [key: string]: string | number };
|
||||
const?: Record<string, string | number>;
|
||||
default?: Record<string, string | number>;
|
||||
secret?: boolean;
|
||||
};
|
||||
|
||||
@@ -169,7 +167,7 @@ export type BlockIOCredentialsSubSchema = BlockIOObjectSubSchema & {
|
||||
credentials_scopes?: string[];
|
||||
credentials_types: Array<CredentialsType>;
|
||||
discriminator?: string;
|
||||
discriminator_mapping?: { [key: string]: CredentialsProviderName };
|
||||
discriminator_mapping?: Record<string, CredentialsProviderName>;
|
||||
discriminator_values?: any[];
|
||||
secret?: boolean;
|
||||
};
|
||||
@@ -215,17 +213,20 @@ export type BlockIODiscriminatedOneOfSubSchema = {
|
||||
secret?: boolean;
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/graph.py:Node */
|
||||
export type Node = {
|
||||
export type NodeCreatable = {
|
||||
id: string;
|
||||
block_id: string;
|
||||
input_default: { [key: string]: any };
|
||||
input_nodes: Array<{ name: string; node_id: string }>;
|
||||
output_nodes: Array<{ name: string; node_id: string }>;
|
||||
input_default: Record<string, any>;
|
||||
metadata: {
|
||||
position: { x: number; y: number };
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/graph.py:Node */
|
||||
export type Node = NodeCreatable & {
|
||||
input_links: Link[];
|
||||
output_links: Link[];
|
||||
webhook?: Webhook;
|
||||
};
|
||||
|
||||
@@ -279,6 +280,7 @@ export type GraphExecution = GraphExecutionMeta & {
|
||||
node_executions?: NodeExecutionResult[];
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/graph.py:GraphMeta */
|
||||
export type GraphMeta = {
|
||||
id: GraphID;
|
||||
user_id: UserID;
|
||||
@@ -290,11 +292,8 @@ export type GraphMeta = {
|
||||
forked_from_version?: number | null;
|
||||
input_schema: GraphIOSchema;
|
||||
output_schema: GraphIOSchema;
|
||||
credentials_input_schema: {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOCredentialsSubSchema };
|
||||
required: (keyof GraphMeta["credentials_input_schema"]["properties"])[];
|
||||
};
|
||||
has_external_trigger: boolean;
|
||||
credentials_input_schema: CredentialsInputSchema;
|
||||
};
|
||||
|
||||
export type GraphID = Brand<string, "GraphID">;
|
||||
@@ -302,7 +301,7 @@ export type GraphID = Brand<string, "GraphID">;
|
||||
/* Derived from backend/data/graph.py:Graph._generate_schema() */
|
||||
export type GraphIOSchema = {
|
||||
type: "object";
|
||||
properties: { [key: string]: GraphIOSubSchema };
|
||||
properties: Record<string, GraphIOSubSchema>;
|
||||
required: (keyof BlockIORootSchema["properties"])[];
|
||||
};
|
||||
export type GraphIOSubSchema = Omit<
|
||||
@@ -315,11 +314,16 @@ export type GraphIOSubSchema = Omit<
|
||||
metadata?: any;
|
||||
};
|
||||
|
||||
export type CredentialsInputSchema = {
|
||||
type: "object";
|
||||
properties: Record<string, BlockIOCredentialsSubSchema>;
|
||||
required: (keyof CredentialsInputSchema["properties"])[];
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/graph.py:Graph */
|
||||
export type Graph = GraphMeta & {
|
||||
nodes: Array<Node>;
|
||||
links: Array<Link>;
|
||||
has_webhook_trigger: boolean;
|
||||
};
|
||||
|
||||
export type GraphUpdateable = Omit<
|
||||
@@ -327,14 +331,16 @@ export type GraphUpdateable = Omit<
|
||||
| "user_id"
|
||||
| "version"
|
||||
| "is_active"
|
||||
| "nodes"
|
||||
| "links"
|
||||
| "input_schema"
|
||||
| "output_schema"
|
||||
| "credentials_input_schema"
|
||||
| "has_webhook_trigger"
|
||||
| "has_external_trigger"
|
||||
> & {
|
||||
version?: number;
|
||||
is_active?: boolean;
|
||||
nodes: Array<NodeCreatable>;
|
||||
links: Array<LinkCreatable>;
|
||||
input_schema?: GraphIOSchema;
|
||||
output_schema?: GraphIOSchema;
|
||||
@@ -357,8 +363,8 @@ export type NodeExecutionResult = {
|
||||
| "COMPLETED"
|
||||
| "TERMINATED"
|
||||
| "FAILED";
|
||||
input_data: { [key: string]: any };
|
||||
output_data: { [key: string]: Array<any> };
|
||||
input_data: Record<string, any>;
|
||||
output_data: Record<string, Array<any>>;
|
||||
add_time: Date;
|
||||
queue_time?: Date;
|
||||
start_time?: Date;
|
||||
@@ -380,31 +386,29 @@ export type LibraryAgent = {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: GraphIOSchema;
|
||||
credentials_input_schema: {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOCredentialsSubSchema };
|
||||
required: (keyof LibraryAgent["credentials_input_schema"]["properties"])[];
|
||||
};
|
||||
credentials_input_schema: CredentialsInputSchema;
|
||||
new_output: boolean;
|
||||
can_access_graph: boolean;
|
||||
is_latest_version: boolean;
|
||||
} & (
|
||||
| {
|
||||
has_external_trigger: true;
|
||||
trigger_setup_info: {
|
||||
provider: CredentialsProviderName;
|
||||
config_schema: BlockIORootSchema;
|
||||
credentials_input_name?: string;
|
||||
};
|
||||
trigger_setup_info: LibraryAgentTriggerInfo;
|
||||
}
|
||||
| {
|
||||
has_external_trigger: false;
|
||||
trigger_setup_info?: null;
|
||||
trigger_setup_info?: undefined;
|
||||
}
|
||||
);
|
||||
|
||||
export type LibraryAgentID = Brand<string, "LibraryAgentID">;
|
||||
|
||||
export type LibraryAgentTriggerInfo = {
|
||||
provider: CredentialsProviderName;
|
||||
config_schema: BlockIORootSchema;
|
||||
credentials_input_name?: string;
|
||||
};
|
||||
|
||||
export enum AgentStatus {
|
||||
COMPLETED = "COMPLETED",
|
||||
HEALTHY = "HEALTHY",
|
||||
@@ -427,7 +431,7 @@ export type LibraryAgentPreset = {
|
||||
updated_at: Date;
|
||||
graph_id: GraphID;
|
||||
graph_version: number;
|
||||
inputs: { [key: string]: any };
|
||||
inputs: Record<string, any>;
|
||||
credentials: Record<string, CredentialsMetaInput>;
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -616,7 +620,7 @@ export type AnalyticsMetrics = {
|
||||
|
||||
export type AnalyticsDetails = {
|
||||
type: string;
|
||||
data: { [key: string]: any };
|
||||
data: Record<string, any>;
|
||||
index: string;
|
||||
};
|
||||
|
||||
@@ -887,7 +891,7 @@ export interface UserOnboarding {
|
||||
integrations: string[];
|
||||
otherIntegrations: string | null;
|
||||
selectedStoreListingVersionId: string | null;
|
||||
agentInput: { [key: string]: string | number } | null;
|
||||
agentInput: Record<string, string | number> | null;
|
||||
onboardingAgentExecutionId: GraphExecutionID | null;
|
||||
agentRuns: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { isEmpty as _isEmpty } from "lodash";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Category } from "@/lib/autogpt-server-api/types";
|
||||
@@ -227,13 +228,6 @@ export function getPrimaryCategoryColor(categories: Category[]): string {
|
||||
);
|
||||
}
|
||||
|
||||
export function filterBlocksByType<T>(
|
||||
blocks: T[],
|
||||
predicate: (block: T) => boolean,
|
||||
): T[] {
|
||||
return blocks.filter(predicate);
|
||||
}
|
||||
|
||||
export enum BehaveAs {
|
||||
CLOUD = "CLOUD",
|
||||
LOCAL = "LOCAL",
|
||||
@@ -397,6 +391,14 @@ export function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
return !str || str.trim().length === 0;
|
||||
}
|
||||
|
||||
export function isEmpty(value: any): boolean {
|
||||
return (
|
||||
value === undefined ||
|
||||
value === "" ||
|
||||
(typeof value === "object" && _isEmpty(value))
|
||||
);
|
||||
}
|
||||
|
||||
/** Check if a value is an object or not */
|
||||
export function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
@@ -21,17 +21,13 @@ test.describe("Build", () => { //(1)!
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await test.expect(page).toHaveURL("/marketplace"); //(5)!
|
||||
await buildPage.navbar.clickBuildLink();
|
||||
await test.expect(page).toHaveURL("/build");
|
||||
await buildPage.waitForPageLoad();
|
||||
});
|
||||
|
||||
// Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
|
||||
// prettier-ignore
|
||||
test("user can add a block", async ({ page }) => { //(6)!
|
||||
// workaround for #8788
|
||||
await buildPage.navbar.clickBuildLink();
|
||||
await test.expect(page).toHaveURL(new RegExp("/build"));
|
||||
await buildPage.waitForPageLoad();
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy(); //(7)!
|
||||
|
||||
test("user can add a block", async () => { //(6)!
|
||||
await buildPage.closeTutorial(); //(9)!
|
||||
await buildPage.openBlocksPanel(); //(10)!
|
||||
const block = await buildPage.getDictionaryBlockDetails();
|
||||
@@ -43,24 +39,24 @@ test.describe("Build", () => { //(1)!
|
||||
// --8<-- [end:BuildPageExample]
|
||||
|
||||
test.skip("user can add all blocks a-l", async ({ page }, testInfo) => {
|
||||
// this test is slow af so we 10x the timeout (sorry future me)
|
||||
await test.setTimeout(testInfo.timeout * 100);
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
||||
// this test is slow af so we 100x the timeout (sorry future me)
|
||||
test.setTimeout(testInfo.timeout * 100);
|
||||
|
||||
await buildPage.closeTutorial();
|
||||
await buildPage.openBlocksPanel();
|
||||
const blocks = await buildPage.getBlocks();
|
||||
|
||||
const blockIdsToSkip = await buildPage.getBlocksToSkip();
|
||||
const blockTypesToSkip = ["Input", "Output", "Agent", "AI"];
|
||||
console.log("⚠️ Skipping blocks:", blockIdsToSkip);
|
||||
console.log("⚠️ Skipping block types:", blockTypesToSkip);
|
||||
|
||||
// add all the blocks in order except for the agent executor block
|
||||
for (const block of blocks) {
|
||||
if (block.name[0].toLowerCase() >= "m") {
|
||||
continue;
|
||||
}
|
||||
if (!blockIdsToSkip.some((b) => b === block.id) && !blockTypesToSkip.some((b) => block.type === b)) {
|
||||
console.log("Adding block:", block.name, block.id, block.type, " skipping types:", blockTypesToSkip);
|
||||
if (!blockIdsToSkip.includes(block.id) && !blockTypesToSkip.includes(block.type)) {
|
||||
await buildPage.addBlock(block);
|
||||
}
|
||||
}
|
||||
@@ -70,8 +66,7 @@ test.describe("Build", () => { //(1)!
|
||||
if (block.name[0].toLowerCase() >= "m") {
|
||||
continue;
|
||||
}
|
||||
if (!blockIdsToSkip.some((b) => b === block.id) && !blockTypesToSkip.some((b) => block.type === b)) {
|
||||
console.log("Checking block:", block.name, block.id, block.type, " skipping types:", blockTypesToSkip);
|
||||
if (!blockIdsToSkip.includes(block.id) && !blockTypesToSkip.includes(block.type)) {
|
||||
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
|
||||
}
|
||||
}
|
||||
@@ -79,28 +74,28 @@ test.describe("Build", () => { //(1)!
|
||||
// check that we can save the agent with all the blocks
|
||||
await buildPage.saveAgent("all blocks test", "all blocks test");
|
||||
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+"));
|
||||
await test.expect(page).toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
|
||||
});
|
||||
|
||||
test.skip("user can add all blocks m-z", async ({ page }, testInfo) => {
|
||||
// this test is slow af so we 10x the timeout (sorry future me)
|
||||
await test.setTimeout(testInfo.timeout * 100);
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
||||
// this test is slow af so we 100x the timeout (sorry future me)
|
||||
test.setTimeout(testInfo.timeout * 100);
|
||||
|
||||
await buildPage.closeTutorial();
|
||||
await buildPage.openBlocksPanel();
|
||||
const blocks = await buildPage.getBlocks();
|
||||
|
||||
const blockIdsToSkip = await buildPage.getBlocksToSkip();
|
||||
const blockTypesToSkip = ["Input", "Output", "Agent", "AI"];
|
||||
console.log("⚠️ Skipping blocks:", blockIdsToSkip);
|
||||
console.log("⚠️ Skipping block types:", blockTypesToSkip);
|
||||
|
||||
// add all the blocks in order except for the agent executor block
|
||||
for (const block of blocks) {
|
||||
if (block.name[0].toLowerCase() < "m") {
|
||||
continue;
|
||||
}
|
||||
if (!blockIdsToSkip.some((b) => b === block.id) && !blockTypesToSkip.some((b) => block.type === b)) {
|
||||
console.log("Adding block:", block.name, block.id, block.type, " skipping types:", blockTypesToSkip);
|
||||
if (!blockIdsToSkip.includes(block.id) && !blockTypesToSkip.includes(block.type)) {
|
||||
await buildPage.addBlock(block);
|
||||
}
|
||||
}
|
||||
@@ -110,8 +105,7 @@ test.describe("Build", () => { //(1)!
|
||||
if (block.name[0].toLowerCase() < "m") {
|
||||
continue;
|
||||
}
|
||||
if (!blockIdsToSkip.some((b) => b === block.id) && !blockTypesToSkip.some((b) => block.type === b)) {
|
||||
console.log("Checking block:", block.name, block.id, block.type, " skipping types:", blockTypesToSkip);
|
||||
if (!blockIdsToSkip.includes(block.id) && !blockTypesToSkip.includes(block.type)) {
|
||||
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
|
||||
}
|
||||
}
|
||||
@@ -119,25 +113,26 @@ test.describe("Build", () => { //(1)!
|
||||
// check that we can save the agent with all the blocks
|
||||
await buildPage.saveAgent("all blocks test", "all blocks test");
|
||||
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+"));
|
||||
await test.expect(page).toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
|
||||
});
|
||||
|
||||
test("build navigation is accessible from navbar", async ({ page }) => {
|
||||
// Navigate somewhere else first
|
||||
await page.goto("/marketplace"); //(4)!
|
||||
|
||||
// Check that navigation to the Builder is available on the page
|
||||
await buildPage.navbar.clickBuildLink();
|
||||
await test.expect(page).toHaveURL(new RegExp("/build"));
|
||||
// workaround for #8788
|
||||
await page.reload();
|
||||
await page.reload();
|
||||
await buildPage.waitForPageLoad();
|
||||
|
||||
await test.expect(page).toHaveURL("/build");
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
test("user can add two blocks and connect them", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await test.setTimeout(testInfo.timeout * 10);
|
||||
test.setTimeout(testInfo.timeout * 10);
|
||||
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
||||
await buildPage.closeTutorial();
|
||||
await buildPage.openBlocksPanel();
|
||||
|
||||
@@ -179,7 +174,7 @@ test.describe("Build", () => { //(1)!
|
||||
"Connected Blocks Test",
|
||||
"Testing block connections",
|
||||
);
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+"));
|
||||
await test.expect(page).toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
|
||||
|
||||
// Wait for the save button to be enabled again
|
||||
await buildPage.waitForSaveButton();
|
||||
@@ -204,9 +199,7 @@ test.describe("Build", () => { //(1)!
|
||||
}) => {
|
||||
// simple calculator to double input and output it
|
||||
|
||||
// load the pages and prep
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
||||
// prep
|
||||
await buildPage.closeTutorial();
|
||||
await buildPage.openBlocksPanel();
|
||||
|
||||
@@ -286,7 +279,7 @@ test.describe("Build", () => { //(1)!
|
||||
"Input and Output Blocks Test",
|
||||
"Testing input and output blocks",
|
||||
);
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+"));
|
||||
await test.expect(page).toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
|
||||
|
||||
// Wait for save to complete
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
@@ -45,7 +45,7 @@ export class BuildPage extends BasePage {
|
||||
name: string = "Test Agent",
|
||||
description: string = "",
|
||||
): Promise<void> {
|
||||
console.log(`saving agent ${name} with description ${description}`);
|
||||
console.log(`💾 Saving agent '${name}' with description '${description}'`);
|
||||
await this.page.getByTestId("blocks-control-save-button").click();
|
||||
await this.page.getByTestId("save-control-name-input").fill(name);
|
||||
await this.page
|
||||
@@ -55,9 +55,11 @@ export class BuildPage extends BasePage {
|
||||
}
|
||||
|
||||
async getBlocks(): Promise<Block[]> {
|
||||
console.log(`getting blocks in sidebar panel`);
|
||||
console.log(`Getting available blocks from sidebar panel`);
|
||||
try {
|
||||
const blocks = await this.page.locator('[data-id^="block-card-"]').all();
|
||||
const blockFinder = this.page.locator('[data-id^="block-card-"]');
|
||||
await blockFinder.first().waitFor();
|
||||
const blocks = await blockFinder.all();
|
||||
|
||||
console.log(`found ${blocks.length} blocks`);
|
||||
|
||||
@@ -97,7 +99,7 @@ export class BuildPage extends BasePage {
|
||||
}
|
||||
|
||||
async addBlock(block: Block): Promise<void> {
|
||||
console.log(`adding block ${block.id} ${block.name} to agent`);
|
||||
console.log(`Adding block ${block.name} (${block.id}) to agent`);
|
||||
await this.page.getByTestId(`block-name-${block.id}`).click();
|
||||
}
|
||||
|
||||
@@ -108,13 +110,11 @@ export class BuildPage extends BasePage {
|
||||
|
||||
async hasBlock(block: Block): Promise<boolean> {
|
||||
console.log(
|
||||
`checking if block ${block.id} ${block.name} is visible on page`,
|
||||
`Checking if block ${block.name} (${block.id}) is visible on page`,
|
||||
);
|
||||
try {
|
||||
// Use both ID and name for most precise matching
|
||||
const node = await this.page
|
||||
.locator(`[data-blockid="${block.id}"]`)
|
||||
.first();
|
||||
const node = this.page.locator(`[data-blockid="${block.id}"]`).first();
|
||||
return await node.isVisible();
|
||||
} catch (error) {
|
||||
console.error("Error checking for block:", error);
|
||||
@@ -123,11 +123,9 @@ export class BuildPage extends BasePage {
|
||||
}
|
||||
|
||||
async getBlockInputs(blockId: string): Promise<string[]> {
|
||||
console.log(`getting block ${blockId} inputs`);
|
||||
console.log(`Getting block ${blockId} inputs`);
|
||||
try {
|
||||
const node = await this.page
|
||||
.locator(`[data-blockid="${blockId}"]`)
|
||||
.first();
|
||||
const node = this.page.locator(`[data-blockid="${blockId}"]`).first();
|
||||
const inputsData = await node.getAttribute("data-inputs");
|
||||
return inputsData ? JSON.parse(inputsData) : [];
|
||||
} catch (error) {
|
||||
@@ -159,9 +157,7 @@ export class BuildPage extends BasePage {
|
||||
|
||||
async getBlockById(blockId: string, dataId?: string): Promise<Locator> {
|
||||
console.log(`getting block ${blockId} with dataId ${dataId}`);
|
||||
return await this.page.locator(
|
||||
await this._buildBlockSelector(blockId, dataId),
|
||||
);
|
||||
return this.page.locator(await this._buildBlockSelector(blockId, dataId));
|
||||
}
|
||||
|
||||
// dataId is optional, if provided, it will start the search with that container, otherwise it will start with the blockId
|
||||
@@ -177,7 +173,7 @@ export class BuildPage extends BasePage {
|
||||
`filling block input ${placeholder} with value ${value} of block ${blockId}`,
|
||||
);
|
||||
const block = await this.getBlockById(blockId, dataId);
|
||||
const input = await block.getByPlaceholder(placeholder);
|
||||
const input = block.getByPlaceholder(placeholder);
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
@@ -222,7 +218,7 @@ export class BuildPage extends BasePage {
|
||||
): Promise<void> {
|
||||
console.log(`filling block input ${label} with value ${value}`);
|
||||
const block = await this.getBlockById(blockId);
|
||||
const input = await block.getByLabel(label);
|
||||
const input = block.getByLabel(label);
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
@@ -235,13 +231,9 @@ export class BuildPage extends BasePage {
|
||||
);
|
||||
try {
|
||||
// Locate the output element
|
||||
const outputElement = await this.page.locator(
|
||||
`[data-id="${blockOutputId}"]`,
|
||||
);
|
||||
const outputElement = this.page.locator(`[data-id="${blockOutputId}"]`);
|
||||
// Locate the input element
|
||||
const inputElement = await this.page.locator(
|
||||
`[data-id="${blockInputId}"]`,
|
||||
);
|
||||
const inputElement = this.page.locator(`[data-id="${blockInputId}"]`);
|
||||
|
||||
await outputElement.dragTo(inputElement);
|
||||
} catch (error) {
|
||||
@@ -303,12 +295,12 @@ export class BuildPage extends BasePage {
|
||||
async fillRunDialog(inputs: Record<string, string>): Promise<void> {
|
||||
console.log(`filling run dialog`);
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
await this.page.getByTestId(`run-dialog-input-${key}`).fill(value);
|
||||
await this.page.getByTestId(`agent-input-${key}`).fill(value);
|
||||
}
|
||||
}
|
||||
async clickRunDialogRunButton(): Promise<void> {
|
||||
console.log(`clicking run button`);
|
||||
await this.page.getByTestId("run-dialog-run-button").click();
|
||||
await this.page.getByTestId("agent-run-button").click();
|
||||
}
|
||||
|
||||
async waitForCompletionBadge(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user