mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
Compare commits
5 Commits
testing-cl
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b25e62959 | ||
|
|
35a13e3df5 | ||
|
|
2169b433c9 | ||
|
|
fa0b7029dd | ||
|
|
c20ca47bb0 |
@@ -218,6 +218,7 @@ async def save_agent_to_library(
|
|||||||
library_agents = await library_db.create_library_agent(
|
library_agents = await library_db.create_library_agent(
|
||||||
graph=created_graph,
|
graph=created_graph,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
sensitive_action_safe_mode=True,
|
||||||
create_library_agents_for_sub_graphs=False,
|
create_library_agents_for_sub_graphs=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from .models import (
|
|||||||
UserReadiness,
|
UserReadiness,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
check_user_has_required_credentials,
|
build_missing_credentials_from_graph,
|
||||||
extract_credentials_from_schema,
|
extract_credentials_from_schema,
|
||||||
fetch_graph_from_store_slug,
|
fetch_graph_from_store_slug,
|
||||||
get_or_create_library_agent,
|
get_or_create_library_agent,
|
||||||
@@ -237,15 +237,13 @@ class RunAgentTool(BaseTool):
|
|||||||
# Return credentials needed response with input data info
|
# Return credentials needed response with input data info
|
||||||
# The UI handles credential setup automatically, so the message
|
# The UI handles credential setup automatically, so the message
|
||||||
# focuses on asking about input data
|
# focuses on asking about input data
|
||||||
credentials = extract_credentials_from_schema(
|
requirements_creds_dict = build_missing_credentials_from_graph(
|
||||||
graph.credentials_input_schema
|
graph, None
|
||||||
)
|
)
|
||||||
missing_creds_check = await check_user_has_required_credentials(
|
missing_credentials_dict = build_missing_credentials_from_graph(
|
||||||
user_id, credentials
|
graph, graph_credentials
|
||||||
)
|
)
|
||||||
missing_credentials_dict = {
|
requirements_creds_list = list(requirements_creds_dict.values())
|
||||||
c.id: c.model_dump() for c in missing_creds_check
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetupRequirementsResponse(
|
return SetupRequirementsResponse(
|
||||||
message=self._build_inputs_message(graph, MSG_WHAT_VALUES_TO_USE),
|
message=self._build_inputs_message(graph, MSG_WHAT_VALUES_TO_USE),
|
||||||
@@ -259,7 +257,7 @@ class RunAgentTool(BaseTool):
|
|||||||
ready_to_run=False,
|
ready_to_run=False,
|
||||||
),
|
),
|
||||||
requirements={
|
requirements={
|
||||||
"credentials": [c.model_dump() for c in credentials],
|
"credentials": requirements_creds_list,
|
||||||
"inputs": self._get_inputs_list(graph.input_schema),
|
"inputs": self._get_inputs_list(graph.input_schema),
|
||||||
"execution_modes": self._get_execution_modes(graph),
|
"execution_modes": self._get_execution_modes(graph),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from .models import (
|
|||||||
ToolResponseBase,
|
ToolResponseBase,
|
||||||
UserReadiness,
|
UserReadiness,
|
||||||
)
|
)
|
||||||
|
from .utils import build_missing_credentials_from_field_info
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -189,7 +190,11 @@ class RunBlockTool(BaseTool):
|
|||||||
|
|
||||||
if missing_credentials:
|
if missing_credentials:
|
||||||
# Return setup requirements response with missing credentials
|
# Return setup requirements response with missing credentials
|
||||||
missing_creds_dict = {c.id: c.model_dump() for c in missing_credentials}
|
credentials_fields_info = block.input_schema.get_credentials_fields_info()
|
||||||
|
missing_creds_dict = build_missing_credentials_from_field_info(
|
||||||
|
credentials_fields_info, set(matched_credentials.keys())
|
||||||
|
)
|
||||||
|
missing_creds_list = list(missing_creds_dict.values())
|
||||||
|
|
||||||
return SetupRequirementsResponse(
|
return SetupRequirementsResponse(
|
||||||
message=(
|
message=(
|
||||||
@@ -206,7 +211,7 @@ class RunBlockTool(BaseTool):
|
|||||||
ready_to_run=False,
|
ready_to_run=False,
|
||||||
),
|
),
|
||||||
requirements={
|
requirements={
|
||||||
"credentials": [c.model_dump() for c in missing_credentials],
|
"credentials": missing_creds_list,
|
||||||
"inputs": self._get_inputs_list(block),
|
"inputs": self._get_inputs_list(block),
|
||||||
"execution_modes": ["immediate"],
|
"execution_modes": ["immediate"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from backend.api.features.library import model as library_model
|
|||||||
from backend.api.features.store import db as store_db
|
from backend.api.features.store import db as store_db
|
||||||
from backend.data import graph as graph_db
|
from backend.data import graph as graph_db
|
||||||
from backend.data.graph import GraphModel
|
from backend.data.graph import GraphModel
|
||||||
from backend.data.model import CredentialsMetaInput
|
from backend.data.model import CredentialsFieldInfo, CredentialsMetaInput
|
||||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||||
from backend.util.exceptions import NotFoundError
|
from backend.util.exceptions import NotFoundError
|
||||||
|
|
||||||
@@ -89,6 +89,59 @@ def extract_credentials_from_schema(
|
|||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_missing_credential(
|
||||||
|
field_key: str, field_info: CredentialsFieldInfo
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convert credential field info into a serializable dict that preserves all supported
|
||||||
|
credential types (e.g., api_key + oauth2) so the UI can offer multiple options.
|
||||||
|
"""
|
||||||
|
supported_types = sorted(field_info.supported_types)
|
||||||
|
provider = next(iter(field_info.provider), "unknown")
|
||||||
|
scopes = sorted(field_info.required_scopes or [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": field_key,
|
||||||
|
"title": field_key.replace("_", " ").title(),
|
||||||
|
"provider": provider,
|
||||||
|
"provider_name": provider.replace("_", " ").title(),
|
||||||
|
"type": supported_types[0] if supported_types else "api_key",
|
||||||
|
"types": supported_types,
|
||||||
|
"scopes": scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_missing_credentials_from_graph(
|
||||||
|
graph: GraphModel, matched_credentials: dict[str, CredentialsMetaInput] | None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Build a missing_credentials mapping from a graph's aggregated credentials inputs,
|
||||||
|
preserving all supported credential types for each field.
|
||||||
|
"""
|
||||||
|
matched_keys = set(matched_credentials.keys()) if matched_credentials else set()
|
||||||
|
aggregated_fields = graph.aggregate_credentials_inputs()
|
||||||
|
|
||||||
|
return {
|
||||||
|
field_key: _serialize_missing_credential(field_key, field_info)
|
||||||
|
for field_key, (field_info, _node_fields) in aggregated_fields.items()
|
||||||
|
if field_key not in matched_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_missing_credentials_from_field_info(
|
||||||
|
credential_fields: dict[str, CredentialsFieldInfo],
|
||||||
|
matched_keys: set[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Build missing_credentials mapping from a simple credentials field info dictionary.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
field_key: _serialize_missing_credential(field_key, field_info)
|
||||||
|
for field_key, field_info in credential_fields.items()
|
||||||
|
if field_key not in matched_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def extract_credentials_as_dict(
|
def extract_credentials_as_dict(
|
||||||
credentials_input_schema: dict[str, Any] | None,
|
credentials_input_schema: dict[str, Any] | None,
|
||||||
) -> dict[str, CredentialsMetaInput]:
|
) -> dict[str, CredentialsMetaInput]:
|
||||||
|
|||||||
@@ -401,27 +401,11 @@ async def add_generated_agent_image(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _initialize_graph_settings(graph: graph_db.GraphModel) -> GraphSettings:
|
|
||||||
"""
|
|
||||||
Initialize GraphSettings based on graph content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
graph: The graph to analyze
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GraphSettings with appropriate human_in_the_loop_safe_mode value
|
|
||||||
"""
|
|
||||||
if graph.has_human_in_the_loop:
|
|
||||||
# Graph has HITL blocks - set safe mode to True by default
|
|
||||||
return GraphSettings(human_in_the_loop_safe_mode=True)
|
|
||||||
else:
|
|
||||||
# Graph has no HITL blocks - keep None
|
|
||||||
return GraphSettings(human_in_the_loop_safe_mode=None)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_library_agent(
|
async def create_library_agent(
|
||||||
graph: graph_db.GraphModel,
|
graph: graph_db.GraphModel,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
hitl_safe_mode: bool = True,
|
||||||
|
sensitive_action_safe_mode: bool = False,
|
||||||
create_library_agents_for_sub_graphs: bool = True,
|
create_library_agents_for_sub_graphs: bool = True,
|
||||||
) -> list[library_model.LibraryAgent]:
|
) -> list[library_model.LibraryAgent]:
|
||||||
"""
|
"""
|
||||||
@@ -430,6 +414,8 @@ async def create_library_agent(
|
|||||||
Args:
|
Args:
|
||||||
agent: The agent/Graph to add to the library.
|
agent: The agent/Graph to add to the library.
|
||||||
user_id: The user to whom the agent will be added.
|
user_id: The user to whom the agent will be added.
|
||||||
|
hitl_safe_mode: Whether HITL blocks require manual review (default True).
|
||||||
|
sensitive_action_safe_mode: Whether sensitive action blocks require review.
|
||||||
create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well.
|
create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -465,7 +451,11 @@ async def create_library_agent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
settings=SafeJson(
|
settings=SafeJson(
|
||||||
_initialize_graph_settings(graph_entry).model_dump()
|
GraphSettings.from_graph(
|
||||||
|
graph_entry,
|
||||||
|
hitl_safe_mode=hitl_safe_mode,
|
||||||
|
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
||||||
|
).model_dump()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
include=library_agent_include(
|
include=library_agent_include(
|
||||||
@@ -627,33 +617,6 @@ async def update_library_agent(
|
|||||||
raise DatabaseError("Failed to update library agent") from e
|
raise DatabaseError("Failed to update library agent") from e
|
||||||
|
|
||||||
|
|
||||||
async def update_library_agent_settings(
|
|
||||||
user_id: str,
|
|
||||||
agent_id: str,
|
|
||||||
settings: GraphSettings,
|
|
||||||
) -> library_model.LibraryAgent:
|
|
||||||
"""
|
|
||||||
Updates the settings for a specific LibraryAgent.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id: The owner of the LibraryAgent.
|
|
||||||
agent_id: The ID of the LibraryAgent to update.
|
|
||||||
settings: New GraphSettings to apply.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The updated LibraryAgent.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundError: If the specified LibraryAgent does not exist.
|
|
||||||
DatabaseError: If there's an error in the update operation.
|
|
||||||
"""
|
|
||||||
return await update_library_agent(
|
|
||||||
library_agent_id=agent_id,
|
|
||||||
user_id=user_id,
|
|
||||||
settings=settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_library_agent(
|
async def delete_library_agent(
|
||||||
library_agent_id: str, user_id: str, soft_delete: bool = True
|
library_agent_id: str, user_id: str, soft_delete: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -838,7 +801,7 @@ async def add_store_agent_to_library(
|
|||||||
"isCreatedByUser": False,
|
"isCreatedByUser": False,
|
||||||
"useGraphIsActiveVersion": False,
|
"useGraphIsActiveVersion": False,
|
||||||
"settings": SafeJson(
|
"settings": SafeJson(
|
||||||
_initialize_graph_settings(graph_model).model_dump()
|
GraphSettings.from_graph(graph_model).model_dump()
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
include=library_agent_include(
|
include=library_agent_include(
|
||||||
@@ -1228,8 +1191,15 @@ async def fork_library_agent(
|
|||||||
)
|
)
|
||||||
new_graph = await on_graph_activate(new_graph, user_id=user_id)
|
new_graph = await on_graph_activate(new_graph, user_id=user_id)
|
||||||
|
|
||||||
# Create a library agent for the new graph
|
# Create a library agent for the new graph, preserving safe mode settings
|
||||||
return (await create_library_agent(new_graph, user_id))[0]
|
return (
|
||||||
|
await create_library_agent(
|
||||||
|
new_graph,
|
||||||
|
user_id,
|
||||||
|
hitl_safe_mode=original_agent.settings.human_in_the_loop_safe_mode,
|
||||||
|
sensitive_action_safe_mode=original_agent.settings.sensitive_action_safe_mode,
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error cloning library agent: {e}")
|
logger.error(f"Database error cloning library agent: {e}")
|
||||||
raise DatabaseError("Failed to fork library agent") from e
|
raise DatabaseError("Failed to fork library agent") from e
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
has_external_trigger: bool = pydantic.Field(
|
has_external_trigger: bool = pydantic.Field(
|
||||||
description="Whether the agent has an external trigger (e.g. webhook) node"
|
description="Whether the agent has an external trigger (e.g. webhook) node"
|
||||||
)
|
)
|
||||||
|
has_human_in_the_loop: bool = pydantic.Field(
|
||||||
|
description="Whether the agent has human-in-the-loop blocks"
|
||||||
|
)
|
||||||
|
has_sensitive_action: bool = pydantic.Field(
|
||||||
|
description="Whether the agent has sensitive action blocks"
|
||||||
|
)
|
||||||
trigger_setup_info: Optional[GraphTriggerInfo] = None
|
trigger_setup_info: Optional[GraphTriggerInfo] = None
|
||||||
|
|
||||||
# Indicates whether there's a new output (based on recent runs)
|
# Indicates whether there's a new output (based on recent runs)
|
||||||
@@ -180,6 +186,8 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
graph.credentials_input_schema if sub_graphs is not None else None
|
graph.credentials_input_schema if sub_graphs is not None else None
|
||||||
),
|
),
|
||||||
has_external_trigger=graph.has_external_trigger,
|
has_external_trigger=graph.has_external_trigger,
|
||||||
|
has_human_in_the_loop=graph.has_human_in_the_loop,
|
||||||
|
has_sensitive_action=graph.has_sensitive_action,
|
||||||
trigger_setup_info=graph.trigger_setup_info,
|
trigger_setup_info=graph.trigger_setup_info,
|
||||||
new_output=new_output,
|
new_output=new_output,
|
||||||
can_access_graph=can_access_graph,
|
can_access_graph=can_access_graph,
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ async def test_get_library_agents_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
|
has_human_in_the_loop=False,
|
||||||
|
has_sensitive_action=False,
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
recommended_schedule_cron=None,
|
recommended_schedule_cron=None,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
@@ -75,6 +77,8 @@ async def test_get_library_agents_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
|
has_human_in_the_loop=False,
|
||||||
|
has_sensitive_action=False,
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
recommended_schedule_cron=None,
|
recommended_schedule_cron=None,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
@@ -150,6 +154,8 @@ async def test_get_favorite_library_agents_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
|
has_human_in_the_loop=False,
|
||||||
|
has_sensitive_action=False,
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
recommended_schedule_cron=None,
|
recommended_schedule_cron=None,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
@@ -218,6 +224,8 @@ def test_add_agent_to_library_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
|
has_human_in_the_loop=False,
|
||||||
|
has_sensitive_action=False,
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
can_access_graph=True,
|
can_access_graph=True,
|
||||||
|
|||||||
@@ -154,15 +154,16 @@ async def store_content_embedding(
|
|||||||
|
|
||||||
# Upsert the embedding
|
# Upsert the embedding
|
||||||
# WHERE clause in DO UPDATE prevents PostgreSQL 15 bug with NULLS NOT DISTINCT
|
# WHERE clause in DO UPDATE prevents PostgreSQL 15 bug with NULLS NOT DISTINCT
|
||||||
|
# Use {pgvector_schema}.vector for explicit pgvector type qualification
|
||||||
await execute_raw_with_schema(
|
await execute_raw_with_schema(
|
||||||
"""
|
"""
|
||||||
INSERT INTO {schema_prefix}"UnifiedContentEmbedding" (
|
INSERT INTO {schema_prefix}"UnifiedContentEmbedding" (
|
||||||
"id", "contentType", "contentId", "userId", "embedding", "searchableText", "metadata", "createdAt", "updatedAt"
|
"id", "contentType", "contentId", "userId", "embedding", "searchableText", "metadata", "createdAt", "updatedAt"
|
||||||
)
|
)
|
||||||
VALUES (gen_random_uuid()::text, $1::{schema_prefix}"ContentType", $2, $3, $4::vector, $5, $6::jsonb, NOW(), NOW())
|
VALUES (gen_random_uuid()::text, $1::{schema_prefix}"ContentType", $2, $3, $4::{pgvector_schema}.vector, $5, $6::jsonb, NOW(), NOW())
|
||||||
ON CONFLICT ("contentType", "contentId", "userId")
|
ON CONFLICT ("contentType", "contentId", "userId")
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
"embedding" = $4::vector,
|
"embedding" = $4::{pgvector_schema}.vector,
|
||||||
"searchableText" = $5,
|
"searchableText" = $5,
|
||||||
"metadata" = $6::jsonb,
|
"metadata" = $6::jsonb,
|
||||||
"updatedAt" = NOW()
|
"updatedAt" = NOW()
|
||||||
@@ -177,7 +178,6 @@ async def store_content_embedding(
|
|||||||
searchable_text,
|
searchable_text,
|
||||||
metadata_json,
|
metadata_json,
|
||||||
client=client,
|
client=client,
|
||||||
set_public_search_path=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Stored embedding for {content_type}:{content_id}")
|
logger.info(f"Stored embedding for {content_type}:{content_id}")
|
||||||
@@ -236,7 +236,6 @@ async def get_content_embedding(
|
|||||||
content_type,
|
content_type,
|
||||||
content_id,
|
content_id,
|
||||||
user_id,
|
user_id,
|
||||||
set_public_search_path=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result and len(result) > 0:
|
if result and len(result) > 0:
|
||||||
@@ -871,31 +870,46 @@ async def semantic_search(
|
|||||||
# Add content type parameters and build placeholders dynamically
|
# Add content type parameters and build placeholders dynamically
|
||||||
content_type_start_idx = len(params) + 1
|
content_type_start_idx = len(params) + 1
|
||||||
content_type_placeholders = ", ".join(
|
content_type_placeholders = ", ".join(
|
||||||
f'${content_type_start_idx + i}::{{{{schema_prefix}}}}"ContentType"'
|
"$" + str(content_type_start_idx + i) + '::{schema_prefix}"ContentType"'
|
||||||
for i in range(len(content_types))
|
for i in range(len(content_types))
|
||||||
)
|
)
|
||||||
params.extend([ct.value for ct in content_types])
|
params.extend([ct.value for ct in content_types])
|
||||||
|
|
||||||
sql = f"""
|
# Build min_similarity param index before appending
|
||||||
|
min_similarity_idx = len(params) + 1
|
||||||
|
params.append(min_similarity)
|
||||||
|
|
||||||
|
# Use regular string (not f-string) for template to preserve {schema_prefix} and {schema} placeholders
|
||||||
|
# Use OPERATOR({pgvector_schema}.<=>) for explicit operator schema qualification
|
||||||
|
sql = (
|
||||||
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
"contentId" as content_id,
|
"contentId" as content_id,
|
||||||
"contentType" as content_type,
|
"contentType" as content_type,
|
||||||
"searchableText" as searchable_text,
|
"searchableText" as searchable_text,
|
||||||
metadata,
|
metadata,
|
||||||
1 - (embedding <=> '{embedding_str}'::vector) as similarity
|
1 - (embedding OPERATOR({pgvector_schema}.<=>) '"""
|
||||||
FROM {{{{schema_prefix}}}}"UnifiedContentEmbedding"
|
+ embedding_str
|
||||||
WHERE "contentType" IN ({content_type_placeholders})
|
+ """'::{pgvector_schema}.vector) as similarity
|
||||||
{user_filter}
|
FROM {schema_prefix}"UnifiedContentEmbedding"
|
||||||
AND 1 - (embedding <=> '{embedding_str}'::vector) >= ${len(params) + 1}
|
WHERE "contentType" IN ("""
|
||||||
|
+ content_type_placeholders
|
||||||
|
+ """)
|
||||||
|
"""
|
||||||
|
+ user_filter
|
||||||
|
+ """
|
||||||
|
AND 1 - (embedding OPERATOR({pgvector_schema}.<=>) '"""
|
||||||
|
+ embedding_str
|
||||||
|
+ """'::{pgvector_schema}.vector) >= $"""
|
||||||
|
+ str(min_similarity_idx)
|
||||||
|
+ """
|
||||||
ORDER BY similarity DESC
|
ORDER BY similarity DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
"""
|
"""
|
||||||
params.append(min_similarity)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = await query_raw_with_schema(
|
results = await query_raw_with_schema(sql, *params)
|
||||||
sql, *params, set_public_search_path=True
|
|
||||||
)
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"content_id": row["content_id"],
|
"content_id": row["content_id"],
|
||||||
@@ -922,31 +936,41 @@ async def semantic_search(
|
|||||||
# Add content type parameters and build placeholders dynamically
|
# Add content type parameters and build placeholders dynamically
|
||||||
content_type_start_idx = len(params_lexical) + 1
|
content_type_start_idx = len(params_lexical) + 1
|
||||||
content_type_placeholders_lexical = ", ".join(
|
content_type_placeholders_lexical = ", ".join(
|
||||||
f'${content_type_start_idx + i}::{{{{schema_prefix}}}}"ContentType"'
|
"$" + str(content_type_start_idx + i) + '::{schema_prefix}"ContentType"'
|
||||||
for i in range(len(content_types))
|
for i in range(len(content_types))
|
||||||
)
|
)
|
||||||
params_lexical.extend([ct.value for ct in content_types])
|
params_lexical.extend([ct.value for ct in content_types])
|
||||||
|
|
||||||
sql_lexical = f"""
|
# Build query param index before appending
|
||||||
|
query_param_idx = len(params_lexical) + 1
|
||||||
|
params_lexical.append(f"%{query}%")
|
||||||
|
|
||||||
|
# Use regular string (not f-string) for template to preserve {schema_prefix} placeholders
|
||||||
|
sql_lexical = (
|
||||||
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
"contentId" as content_id,
|
"contentId" as content_id,
|
||||||
"contentType" as content_type,
|
"contentType" as content_type,
|
||||||
"searchableText" as searchable_text,
|
"searchableText" as searchable_text,
|
||||||
metadata,
|
metadata,
|
||||||
0.0 as similarity
|
0.0 as similarity
|
||||||
FROM {{{{schema_prefix}}}}"UnifiedContentEmbedding"
|
FROM {schema_prefix}"UnifiedContentEmbedding"
|
||||||
WHERE "contentType" IN ({content_type_placeholders_lexical})
|
WHERE "contentType" IN ("""
|
||||||
{user_filter}
|
+ content_type_placeholders_lexical
|
||||||
AND "searchableText" ILIKE ${len(params_lexical) + 1}
|
+ """)
|
||||||
|
"""
|
||||||
|
+ user_filter
|
||||||
|
+ """
|
||||||
|
AND "searchableText" ILIKE $"""
|
||||||
|
+ str(query_param_idx)
|
||||||
|
+ """
|
||||||
ORDER BY "updatedAt" DESC
|
ORDER BY "updatedAt" DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
"""
|
"""
|
||||||
params_lexical.append(f"%{query}%")
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = await query_raw_with_schema(
|
results = await query_raw_with_schema(sql_lexical, *params_lexical)
|
||||||
sql_lexical, *params_lexical, set_public_search_path=True
|
|
||||||
)
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"content_id": row["content_id"],
|
"content_id": row["content_id"],
|
||||||
|
|||||||
@@ -155,18 +155,14 @@ async def test_store_embedding_success(mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
# execute_raw is called twice: once for SET search_path, once for INSERT
|
# execute_raw is called once for INSERT (no separate SET search_path needed)
|
||||||
assert mock_client.execute_raw.call_count == 2
|
assert mock_client.execute_raw.call_count == 1
|
||||||
|
|
||||||
# First call: SET search_path
|
# Verify the INSERT query with the actual data
|
||||||
first_call_args = mock_client.execute_raw.call_args_list[0][0]
|
call_args = mock_client.execute_raw.call_args_list[0][0]
|
||||||
assert "SET search_path" in first_call_args[0]
|
assert "test-version-id" in call_args
|
||||||
|
assert "[0.1,0.2,0.3]" in call_args
|
||||||
# Second call: INSERT query with the actual data
|
assert None in call_args # userId should be None for store agents
|
||||||
second_call_args = mock_client.execute_raw.call_args_list[1][0]
|
|
||||||
assert "test-version-id" in second_call_args
|
|
||||||
assert "[0.1,0.2,0.3]" in second_call_args
|
|
||||||
assert None in second_call_args # userId should be None for store agents
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(loop_scope="session")
|
@pytest.mark.asyncio(loop_scope="session")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from prisma.enums import ContentType
|
from prisma.enums import ContentType
|
||||||
from rank_bm25 import BM25Okapi
|
from rank_bm25 import BM25Okapi # type: ignore[import-untyped]
|
||||||
|
|
||||||
from backend.api.features.store.embeddings import (
|
from backend.api.features.store.embeddings import (
|
||||||
EMBEDDING_DIM,
|
EMBEDDING_DIM,
|
||||||
@@ -295,7 +295,7 @@ async def unified_hybrid_search(
|
|||||||
FROM {{schema_prefix}}"UnifiedContentEmbedding" uce
|
FROM {{schema_prefix}}"UnifiedContentEmbedding" uce
|
||||||
WHERE uce."contentType" = ANY({content_types_param}::{{schema_prefix}}"ContentType"[])
|
WHERE uce."contentType" = ANY({content_types_param}::{{schema_prefix}}"ContentType"[])
|
||||||
{user_filter}
|
{user_filter}
|
||||||
ORDER BY uce.embedding <=> {embedding_param}::vector
|
ORDER BY uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector
|
||||||
LIMIT 200
|
LIMIT 200
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -307,7 +307,7 @@ async def unified_hybrid_search(
|
|||||||
uce.metadata,
|
uce.metadata,
|
||||||
uce."updatedAt" as updated_at,
|
uce."updatedAt" as updated_at,
|
||||||
-- Semantic score: cosine similarity (1 - distance)
|
-- Semantic score: cosine similarity (1 - distance)
|
||||||
COALESCE(1 - (uce.embedding <=> {embedding_param}::vector), 0) as semantic_score,
|
COALESCE(1 - (uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector), 0) as semantic_score,
|
||||||
-- Lexical score: ts_rank_cd
|
-- Lexical score: ts_rank_cd
|
||||||
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
|
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
|
||||||
-- Category match from metadata
|
-- Category match from metadata
|
||||||
@@ -363,9 +363,7 @@ async def unified_hybrid_search(
|
|||||||
LIMIT {limit_param} OFFSET {offset_param}
|
LIMIT {limit_param} OFFSET {offset_param}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = await query_raw_with_schema(
|
results = await query_raw_with_schema(sql_query, *params)
|
||||||
sql_query, *params, set_public_search_path=True
|
|
||||||
)
|
|
||||||
|
|
||||||
total = results[0]["total_count"] if results else 0
|
total = results[0]["total_count"] if results else 0
|
||||||
# Apply BM25 reranking
|
# Apply BM25 reranking
|
||||||
@@ -585,7 +583,7 @@ async def hybrid_search(
|
|||||||
WHERE uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
|
WHERE uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
|
||||||
AND uce."userId" IS NULL
|
AND uce."userId" IS NULL
|
||||||
AND {where_clause}
|
AND {where_clause}
|
||||||
ORDER BY uce.embedding <=> {embedding_param}::vector
|
ORDER BY uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector
|
||||||
LIMIT 200
|
LIMIT 200
|
||||||
) uce
|
) uce
|
||||||
),
|
),
|
||||||
@@ -607,7 +605,7 @@ async def hybrid_search(
|
|||||||
-- Searchable text for BM25 reranking
|
-- Searchable text for BM25 reranking
|
||||||
COALESCE(sa.agent_name, '') || ' ' || COALESCE(sa.sub_heading, '') || ' ' || COALESCE(sa.description, '') as searchable_text,
|
COALESCE(sa.agent_name, '') || ' ' || COALESCE(sa.sub_heading, '') || ' ' || COALESCE(sa.description, '') as searchable_text,
|
||||||
-- Semantic score
|
-- Semantic score
|
||||||
COALESCE(1 - (uce.embedding <=> {embedding_param}::vector), 0) as semantic_score,
|
COALESCE(1 - (uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector), 0) as semantic_score,
|
||||||
-- Lexical score (raw, will normalize)
|
-- Lexical score (raw, will normalize)
|
||||||
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
|
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
|
||||||
-- Category match
|
-- Category match
|
||||||
@@ -688,9 +686,7 @@ async def hybrid_search(
|
|||||||
LIMIT {limit_param} OFFSET {offset_param}
|
LIMIT {limit_param} OFFSET {offset_param}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = await query_raw_with_schema(
|
results = await query_raw_with_schema(sql_query, *params)
|
||||||
sql_query, *params, set_public_search_path=True
|
|
||||||
)
|
|
||||||
|
|
||||||
total = results[0]["total_count"] if results else 0
|
total = results[0]["total_count"] if results else 0
|
||||||
|
|
||||||
|
|||||||
@@ -761,10 +761,8 @@ async def create_new_graph(
|
|||||||
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
||||||
graph.validate_graph(for_run=False)
|
graph.validate_graph(for_run=False)
|
||||||
|
|
||||||
# The return value of the create graph & library function is intentionally not used here,
|
|
||||||
# as the graph already valid and no sub-graphs are returned back.
|
|
||||||
await graph_db.create_graph(graph, user_id=user_id)
|
await graph_db.create_graph(graph, user_id=user_id)
|
||||||
await library_db.create_library_agent(graph, user_id=user_id)
|
await library_db.create_library_agent(graph, user_id)
|
||||||
activated_graph = await on_graph_activate(graph, user_id=user_id)
|
activated_graph = await on_graph_activate(graph, user_id=user_id)
|
||||||
|
|
||||||
if create_graph.source == "builder":
|
if create_graph.source == "builder":
|
||||||
@@ -888,21 +886,19 @@ async def set_graph_active_version(
|
|||||||
async def _update_library_agent_version_and_settings(
|
async def _update_library_agent_version_and_settings(
|
||||||
user_id: str, agent_graph: graph_db.GraphModel
|
user_id: str, agent_graph: graph_db.GraphModel
|
||||||
) -> library_model.LibraryAgent:
|
) -> library_model.LibraryAgent:
|
||||||
# Keep the library agent up to date with the new active version
|
|
||||||
library = await library_db.update_agent_version_in_library(
|
library = await library_db.update_agent_version_in_library(
|
||||||
user_id, agent_graph.id, agent_graph.version
|
user_id, agent_graph.id, agent_graph.version
|
||||||
)
|
)
|
||||||
# If the graph has HITL node, initialize the setting if it's not already set.
|
updated_settings = GraphSettings.from_graph(
|
||||||
if (
|
graph=agent_graph,
|
||||||
agent_graph.has_human_in_the_loop
|
hitl_safe_mode=library.settings.human_in_the_loop_safe_mode,
|
||||||
and library.settings.human_in_the_loop_safe_mode is None
|
sensitive_action_safe_mode=library.settings.sensitive_action_safe_mode,
|
||||||
):
|
)
|
||||||
await library_db.update_library_agent_settings(
|
if updated_settings != library.settings:
|
||||||
|
library = await library_db.update_library_agent(
|
||||||
|
library_agent_id=library.id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
agent_id=library.id,
|
settings=updated_settings,
|
||||||
settings=library.settings.model_copy(
|
|
||||||
update={"human_in_the_loop_safe_mode": True}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return library
|
return library
|
||||||
|
|
||||||
@@ -919,21 +915,18 @@ async def update_graph_settings(
|
|||||||
user_id: Annotated[str, Security(get_user_id)],
|
user_id: Annotated[str, Security(get_user_id)],
|
||||||
) -> GraphSettings:
|
) -> GraphSettings:
|
||||||
"""Update graph settings for the user's library agent."""
|
"""Update graph settings for the user's library agent."""
|
||||||
# Get the library agent for this graph
|
|
||||||
library_agent = await library_db.get_library_agent_by_graph_id(
|
library_agent = await library_db.get_library_agent_by_graph_id(
|
||||||
graph_id=graph_id, user_id=user_id
|
graph_id=graph_id, user_id=user_id
|
||||||
)
|
)
|
||||||
if not library_agent:
|
if not library_agent:
|
||||||
raise HTTPException(404, f"Graph #{graph_id} not found in user's library")
|
raise HTTPException(404, f"Graph #{graph_id} not found in user's library")
|
||||||
|
|
||||||
# Update the library agent settings
|
updated_agent = await library_db.update_library_agent(
|
||||||
updated_agent = await library_db.update_library_agent_settings(
|
library_agent_id=library_agent.id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
agent_id=library_agent.id,
|
|
||||||
settings=settings,
|
settings=settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return the updated settings
|
|
||||||
return GraphSettings.model_validate(updated_agent.settings)
|
return GraphSettings.model_validate(updated_agent.settings)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -680,3 +680,58 @@ class ListIsEmptyBlock(Block):
|
|||||||
|
|
||||||
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||||
yield "is_empty", len(input_data.list) == 0
|
yield "is_empty", len(input_data.list) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class ConcatenateListsBlock(Block):
|
||||||
|
class Input(BlockSchemaInput):
|
||||||
|
lists: List[List[Any]] = SchemaField(
|
||||||
|
description="A list of lists to concatenate together. All lists will be combined in order into a single list.",
|
||||||
|
placeholder="e.g., [[1, 2], [3, 4], [5, 6]]",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Output(BlockSchemaOutput):
|
||||||
|
concatenated_list: List[Any] = SchemaField(
|
||||||
|
description="The concatenated list containing all elements from all input lists in order."
|
||||||
|
)
|
||||||
|
error: str = SchemaField(
|
||||||
|
description="Error message if concatenation failed due to invalid input types."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="3cf9298b-5817-4141-9d80-7c2cc5199c8e",
|
||||||
|
description="Concatenates multiple lists into a single list. All elements from all input lists are combined in order.",
|
||||||
|
categories={BlockCategory.BASIC},
|
||||||
|
input_schema=ConcatenateListsBlock.Input,
|
||||||
|
output_schema=ConcatenateListsBlock.Output,
|
||||||
|
test_input=[
|
||||||
|
{"lists": [[1, 2, 3], [4, 5, 6]]},
|
||||||
|
{"lists": [["a", "b"], ["c"], ["d", "e", "f"]]},
|
||||||
|
{"lists": [[1, 2], []]},
|
||||||
|
{"lists": []},
|
||||||
|
],
|
||||||
|
test_output=[
|
||||||
|
("concatenated_list", [1, 2, 3, 4, 5, 6]),
|
||||||
|
("concatenated_list", ["a", "b", "c", "d", "e", "f"]),
|
||||||
|
("concatenated_list", [1, 2]),
|
||||||
|
("concatenated_list", []),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||||
|
concatenated = []
|
||||||
|
for idx, lst in enumerate(input_data.lists):
|
||||||
|
if lst is None:
|
||||||
|
# Skip None values to avoid errors
|
||||||
|
continue
|
||||||
|
if not isinstance(lst, list):
|
||||||
|
# Type validation: each item must be a list
|
||||||
|
# Strings are iterable and would cause extend() to iterate character-by-character
|
||||||
|
# Non-iterable types would raise TypeError
|
||||||
|
yield "error", (
|
||||||
|
f"Invalid input at index {idx}: expected a list, got {type(lst).__name__}. "
|
||||||
|
f"All items in 'lists' must be lists (e.g., [[1, 2], [3, 4]])."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
concatenated.extend(lst)
|
||||||
|
yield "concatenated_list", concatenated
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class HITLReviewHelper:
|
|||||||
Exception: If review creation or status update fails
|
Exception: If review creation or status update fails
|
||||||
"""
|
"""
|
||||||
# Skip review if safe mode is disabled - return auto-approved result
|
# Skip review if safe mode is disabled - return auto-approved result
|
||||||
if not execution_context.safe_mode:
|
if not execution_context.human_in_the_loop_safe_mode:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Block {block_name} skipping review for node {node_exec_id} - safe mode disabled"
|
f"Block {block_name} skipping review for node {node_exec_id} - safe mode disabled"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class HumanInTheLoopBlock(Block):
|
|||||||
execution_context: ExecutionContext,
|
execution_context: ExecutionContext,
|
||||||
**_kwargs,
|
**_kwargs,
|
||||||
) -> BlockOutput:
|
) -> BlockOutput:
|
||||||
if not execution_context.safe_mode:
|
if not execution_context.human_in_the_loop_safe_mode:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
|
f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ async def test_smart_decision_maker_tracks_llm_stats():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -535,7 +535,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -730,7 +730,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -786,7 +786,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -905,7 +905,7 @@ async def test_smart_decision_maker_agent_mode():
|
|||||||
# Create a mock execution context
|
# Create a mock execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(
|
mock_execution_context = ExecutionContext(
|
||||||
safe_mode=False,
|
human_in_the_loop_safe_mode=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a mock execution processor for agent mode tests
|
# Create a mock execution processor for agent mode tests
|
||||||
@@ -1027,7 +1027,7 @@ async def test_smart_decision_maker_traditional_mode_default():
|
|||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ async def test_output_yielding_with_dynamic_fields():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
from backend.data.execution import ExecutionContext
|
from backend.data.execution import ExecutionContext
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
||||||
mock_execution_processor = MagicMock()
|
mock_execution_processor = MagicMock()
|
||||||
|
|
||||||
async for output_name, output_value in block.run(
|
async for output_name, output_value in block.run(
|
||||||
@@ -609,7 +609,9 @@ async def test_validation_errors_dont_pollute_conversation():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
from backend.data.execution import ExecutionContext
|
from backend.data.execution import ExecutionContext
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(safe_mode=False)
|
mock_execution_context = ExecutionContext(
|
||||||
|
human_in_the_loop_safe_mode=False
|
||||||
|
)
|
||||||
|
|
||||||
# Create a proper mock execution processor for agent mode
|
# Create a proper mock execution processor for agent mode
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|||||||
@@ -474,7 +474,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
|||||||
self.block_type = block_type
|
self.block_type = block_type
|
||||||
self.webhook_config = webhook_config
|
self.webhook_config = webhook_config
|
||||||
self.execution_stats: NodeExecutionStats = NodeExecutionStats()
|
self.execution_stats: NodeExecutionStats = NodeExecutionStats()
|
||||||
self.requires_human_review: bool = False
|
self.is_sensitive_action: bool = False
|
||||||
|
|
||||||
if self.webhook_config:
|
if self.webhook_config:
|
||||||
if isinstance(self.webhook_config, BlockWebhookConfig):
|
if isinstance(self.webhook_config, BlockWebhookConfig):
|
||||||
@@ -637,8 +637,9 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
|||||||
- should_pause: True if execution should be paused for review
|
- should_pause: True if execution should be paused for review
|
||||||
- input_data_to_use: The input data to use (may be modified by reviewer)
|
- input_data_to_use: The input data to use (may be modified by reviewer)
|
||||||
"""
|
"""
|
||||||
# Skip review if not required or safe mode is disabled
|
if not (
|
||||||
if not self.requires_human_review or not execution_context.safe_mode:
|
self.is_sensitive_action and execution_context.sensitive_action_safe_mode
|
||||||
|
):
|
||||||
return False, input_data
|
return False, input_data
|
||||||
|
|
||||||
from backend.blocks.helpers.review import HITLReviewHelper
|
from backend.blocks.helpers.review import HITLReviewHelper
|
||||||
|
|||||||
@@ -38,20 +38,6 @@ POOL_TIMEOUT = os.getenv("DB_POOL_TIMEOUT")
|
|||||||
if POOL_TIMEOUT:
|
if POOL_TIMEOUT:
|
||||||
DATABASE_URL = add_param(DATABASE_URL, "pool_timeout", POOL_TIMEOUT)
|
DATABASE_URL = add_param(DATABASE_URL, "pool_timeout", POOL_TIMEOUT)
|
||||||
|
|
||||||
# Add public schema to search_path for pgvector type access
|
|
||||||
# The vector extension is in public schema, but search_path is determined by schema parameter
|
|
||||||
# Extract the schema from DATABASE_URL or default to 'public' (matching get_database_schema())
|
|
||||||
parsed_url = urlparse(DATABASE_URL)
|
|
||||||
url_params = dict(parse_qsl(parsed_url.query))
|
|
||||||
db_schema = url_params.get("schema", "public")
|
|
||||||
# Build search_path, avoiding duplicates if db_schema is already 'public'
|
|
||||||
search_path_schemas = list(
|
|
||||||
dict.fromkeys([db_schema, "public"])
|
|
||||||
) # Preserves order, removes duplicates
|
|
||||||
search_path = ",".join(search_path_schemas)
|
|
||||||
# This allows using ::vector without schema qualification
|
|
||||||
DATABASE_URL = add_param(DATABASE_URL, "options", f"-c search_path={search_path}")
|
|
||||||
|
|
||||||
HTTP_TIMEOUT = int(POOL_TIMEOUT) if POOL_TIMEOUT else None
|
HTTP_TIMEOUT = int(POOL_TIMEOUT) if POOL_TIMEOUT else None
|
||||||
|
|
||||||
prisma = Prisma(
|
prisma = Prisma(
|
||||||
@@ -127,38 +113,48 @@ async def _raw_with_schema(
|
|||||||
*args,
|
*args,
|
||||||
execute: bool = False,
|
execute: bool = False,
|
||||||
client: Prisma | None = None,
|
client: Prisma | None = None,
|
||||||
set_public_search_path: bool = False,
|
|
||||||
) -> list[dict] | int:
|
) -> list[dict] | int:
|
||||||
"""Internal: Execute raw SQL with proper schema handling.
|
"""Internal: Execute raw SQL with proper schema handling.
|
||||||
|
|
||||||
Use query_raw_with_schema() or execute_raw_with_schema() instead.
|
Use query_raw_with_schema() or execute_raw_with_schema() instead.
|
||||||
|
|
||||||
|
Supports placeholders:
|
||||||
|
- {schema_prefix}: Table/type prefix (e.g., "platform".)
|
||||||
|
- {schema}: Raw schema name for application tables (e.g., platform)
|
||||||
|
- {pgvector_schema}: Schema where pgvector is installed (defaults to "public")
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_template: SQL query with {schema_prefix} placeholder
|
query_template: SQL query with {schema_prefix}, {schema}, and/or {pgvector_schema} placeholders
|
||||||
*args: Query parameters
|
*args: Query parameters
|
||||||
execute: If False, executes SELECT query. If True, executes INSERT/UPDATE/DELETE.
|
execute: If False, executes SELECT query. If True, executes INSERT/UPDATE/DELETE.
|
||||||
client: Optional Prisma client for transactions (only used when execute=True).
|
client: Optional Prisma client for transactions (only used when execute=True).
|
||||||
set_public_search_path: If True, sets search_path to include public schema.
|
|
||||||
Needed for pgvector types and other public schema objects.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- list[dict] if execute=False (query results)
|
- list[dict] if execute=False (query results)
|
||||||
- int if execute=True (number of affected rows)
|
- int if execute=True (number of affected rows)
|
||||||
|
|
||||||
|
Example with vector type:
|
||||||
|
await execute_raw_with_schema(
|
||||||
|
'INSERT INTO {schema_prefix}"Embedding" (vec) VALUES ($1::{pgvector_schema}.vector)',
|
||||||
|
embedding_data
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
schema = get_database_schema()
|
schema = get_database_schema()
|
||||||
schema_prefix = f'"{schema}".' if schema != "public" else ""
|
schema_prefix = f'"{schema}".' if schema != "public" else ""
|
||||||
formatted_query = query_template.format(schema_prefix=schema_prefix)
|
# pgvector extension is typically installed in "public" schema
|
||||||
|
# On Supabase it may be in "extensions" but "public" is the common default
|
||||||
|
pgvector_schema = "public"
|
||||||
|
|
||||||
|
formatted_query = query_template.format(
|
||||||
|
schema_prefix=schema_prefix,
|
||||||
|
schema=schema,
|
||||||
|
pgvector_schema=pgvector_schema,
|
||||||
|
)
|
||||||
|
|
||||||
import prisma as prisma_module
|
import prisma as prisma_module
|
||||||
|
|
||||||
db_client = client if client else prisma_module.get_client()
|
db_client = client if client else prisma_module.get_client()
|
||||||
|
|
||||||
# Set search_path to include public schema if requested
|
|
||||||
# Prisma doesn't support the 'options' connection parameter, so we set it per-session
|
|
||||||
# This is idempotent and safe to call multiple times
|
|
||||||
if set_public_search_path:
|
|
||||||
await db_client.execute_raw(f"SET search_path = {schema}, public") # type: ignore
|
|
||||||
|
|
||||||
if execute:
|
if execute:
|
||||||
result = await db_client.execute_raw(formatted_query, *args) # type: ignore
|
result = await db_client.execute_raw(formatted_query, *args) # type: ignore
|
||||||
else:
|
else:
|
||||||
@@ -167,16 +163,12 @@ async def _raw_with_schema(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def query_raw_with_schema(
|
async def query_raw_with_schema(query_template: str, *args) -> list[dict]:
|
||||||
query_template: str, *args, set_public_search_path: bool = False
|
|
||||||
) -> list[dict]:
|
|
||||||
"""Execute raw SQL SELECT query with proper schema handling.
|
"""Execute raw SQL SELECT query with proper schema handling.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_template: SQL query with {schema_prefix} placeholder
|
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
|
||||||
*args: Query parameters
|
*args: Query parameters
|
||||||
set_public_search_path: If True, sets search_path to include public schema.
|
|
||||||
Needed for pgvector types and other public schema objects.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of result rows as dictionaries
|
List of result rows as dictionaries
|
||||||
@@ -187,23 +179,20 @@ async def query_raw_with_schema(
|
|||||||
user_id
|
user_id
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
return await _raw_with_schema(query_template, *args, execute=False, set_public_search_path=set_public_search_path) # type: ignore
|
return await _raw_with_schema(query_template, *args, execute=False) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def execute_raw_with_schema(
|
async def execute_raw_with_schema(
|
||||||
query_template: str,
|
query_template: str,
|
||||||
*args,
|
*args,
|
||||||
client: Prisma | None = None,
|
client: Prisma | None = None,
|
||||||
set_public_search_path: bool = False,
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Execute raw SQL command (INSERT/UPDATE/DELETE) with proper schema handling.
|
"""Execute raw SQL command (INSERT/UPDATE/DELETE) with proper schema handling.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_template: SQL query with {schema_prefix} placeholder
|
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
|
||||||
*args: Query parameters
|
*args: Query parameters
|
||||||
client: Optional Prisma client for transactions
|
client: Optional Prisma client for transactions
|
||||||
set_public_search_path: If True, sets search_path to include public schema.
|
|
||||||
Needed for pgvector types and other public schema objects.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of affected rows
|
Number of affected rows
|
||||||
@@ -215,7 +204,7 @@ async def execute_raw_with_schema(
|
|||||||
client=tx # Optional transaction client
|
client=tx # Optional transaction client
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
return await _raw_with_schema(query_template, *args, execute=True, client=client, set_public_search_path=set_public_search_path) # type: ignore
|
return await _raw_with_schema(query_template, *args, execute=True, client=client) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class BaseDbModel(BaseModel):
|
class BaseDbModel(BaseModel):
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class ExecutionContext(BaseModel):
|
|||||||
This includes information needed by blocks, sub-graphs, and execution management.
|
This includes information needed by blocks, sub-graphs, and execution management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
safe_mode: bool = True
|
human_in_the_loop_safe_mode: bool = True
|
||||||
|
sensitive_action_safe_mode: bool = False
|
||||||
user_timezone: str = "UTC"
|
user_timezone: str = "UTC"
|
||||||
root_execution_id: Optional[str] = None
|
root_execution_id: Optional[str] = None
|
||||||
parent_execution_id: Optional[str] = None
|
parent_execution_id: Optional[str] = None
|
||||||
|
|||||||
@@ -62,7 +62,23 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class GraphSettings(BaseModel):
|
class GraphSettings(BaseModel):
|
||||||
human_in_the_loop_safe_mode: bool | None = None
|
human_in_the_loop_safe_mode: bool = True
|
||||||
|
sensitive_action_safe_mode: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_graph(
|
||||||
|
cls,
|
||||||
|
graph: "GraphModel",
|
||||||
|
hitl_safe_mode: bool | None = None,
|
||||||
|
sensitive_action_safe_mode: bool = False,
|
||||||
|
) -> "GraphSettings":
|
||||||
|
# Default to True if not explicitly set
|
||||||
|
if hitl_safe_mode is None:
|
||||||
|
hitl_safe_mode = True
|
||||||
|
return cls(
|
||||||
|
human_in_the_loop_safe_mode=hitl_safe_mode,
|
||||||
|
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Link(BaseDbModel):
|
class Link(BaseDbModel):
|
||||||
@@ -244,10 +260,14 @@ class BaseGraph(BaseDbModel):
|
|||||||
return any(
|
return any(
|
||||||
node.block_id
|
node.block_id
|
||||||
for node in self.nodes
|
for node in self.nodes
|
||||||
if (
|
if node.block.block_type == BlockType.HUMAN_IN_THE_LOOP
|
||||||
node.block.block_type == BlockType.HUMAN_IN_THE_LOOP
|
|
||||||
or node.block.requires_human_review
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def has_sensitive_action(self) -> bool:
|
||||||
|
return any(
|
||||||
|
node.block_id for node in self.nodes if node.block.is_sensitive_action
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ def ensure_embeddings_coverage():
|
|||||||
|
|
||||||
# Process in batches until no more missing embeddings
|
# Process in batches until no more missing embeddings
|
||||||
while True:
|
while True:
|
||||||
result = db_client.backfill_missing_embeddings(batch_size=10)
|
result = db_client.backfill_missing_embeddings(batch_size=100)
|
||||||
|
|
||||||
total_processed += result["processed"]
|
total_processed += result["processed"]
|
||||||
total_success += result["success"]
|
total_success += result["success"]
|
||||||
|
|||||||
@@ -873,11 +873,8 @@ async def add_graph_execution(
|
|||||||
settings = await gdb.get_graph_settings(user_id=user_id, graph_id=graph_id)
|
settings = await gdb.get_graph_settings(user_id=user_id, graph_id=graph_id)
|
||||||
|
|
||||||
execution_context = ExecutionContext(
|
execution_context = ExecutionContext(
|
||||||
safe_mode=(
|
human_in_the_loop_safe_mode=settings.human_in_the_loop_safe_mode,
|
||||||
settings.human_in_the_loop_safe_mode
|
sensitive_action_safe_mode=settings.sensitive_action_safe_mode,
|
||||||
if settings.human_in_the_loop_safe_mode is not None
|
|
||||||
else True
|
|
||||||
),
|
|
||||||
user_timezone=(
|
user_timezone=(
|
||||||
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
|
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ async def test_add_graph_execution_is_repeatable(mocker: MockerFixture):
|
|||||||
mock_user.timezone = "UTC"
|
mock_user.timezone = "UTC"
|
||||||
mock_settings = mocker.MagicMock()
|
mock_settings = mocker.MagicMock()
|
||||||
mock_settings.human_in_the_loop_safe_mode = True
|
mock_settings.human_in_the_loop_safe_mode = True
|
||||||
|
mock_settings.sensitive_action_safe_mode = False
|
||||||
|
|
||||||
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
||||||
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
||||||
@@ -651,6 +652,7 @@ async def test_add_graph_execution_with_nodes_to_skip(mocker: MockerFixture):
|
|||||||
mock_user.timezone = "UTC"
|
mock_user.timezone = "UTC"
|
||||||
mock_settings = mocker.MagicMock()
|
mock_settings = mocker.MagicMock()
|
||||||
mock_settings.human_in_the_loop_safe_mode = True
|
mock_settings.human_in_the_loop_safe_mode = True
|
||||||
|
mock_settings.sensitive_action_safe_mode = False
|
||||||
|
|
||||||
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
||||||
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"forked_from_version": null,
|
"forked_from_version": null,
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
"has_human_in_the_loop": false,
|
"has_human_in_the_loop": false,
|
||||||
|
"has_sensitive_action": false,
|
||||||
"id": "graph-123",
|
"id": "graph-123",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"properties": {},
|
"properties": {},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"forked_from_version": null,
|
"forked_from_version": null,
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
"has_human_in_the_loop": false,
|
"has_human_in_the_loop": false,
|
||||||
|
"has_sensitive_action": false,
|
||||||
"id": "graph-123",
|
"id": "graph-123",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"properties": {},
|
"properties": {},
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"properties": {}
|
"properties": {}
|
||||||
},
|
},
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
|
"has_human_in_the_loop": false,
|
||||||
|
"has_sensitive_action": false,
|
||||||
"trigger_setup_info": null,
|
"trigger_setup_info": null,
|
||||||
"new_output": false,
|
"new_output": false,
|
||||||
"can_access_graph": true,
|
"can_access_graph": true,
|
||||||
@@ -34,7 +36,8 @@
|
|||||||
"is_favorite": false,
|
"is_favorite": false,
|
||||||
"recommended_schedule_cron": null,
|
"recommended_schedule_cron": null,
|
||||||
"settings": {
|
"settings": {
|
||||||
"human_in_the_loop_safe_mode": null
|
"human_in_the_loop_safe_mode": true,
|
||||||
|
"sensitive_action_safe_mode": false
|
||||||
},
|
},
|
||||||
"marketplace_listing": null
|
"marketplace_listing": null
|
||||||
},
|
},
|
||||||
@@ -65,6 +68,8 @@
|
|||||||
"properties": {}
|
"properties": {}
|
||||||
},
|
},
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
|
"has_human_in_the_loop": false,
|
||||||
|
"has_sensitive_action": false,
|
||||||
"trigger_setup_info": null,
|
"trigger_setup_info": null,
|
||||||
"new_output": false,
|
"new_output": false,
|
||||||
"can_access_graph": false,
|
"can_access_graph": false,
|
||||||
@@ -72,7 +77,8 @@
|
|||||||
"is_favorite": false,
|
"is_favorite": false,
|
||||||
"recommended_schedule_cron": null,
|
"recommended_schedule_cron": null,
|
||||||
"settings": {
|
"settings": {
|
||||||
"human_in_the_loop_safe_mode": null
|
"human_in_the_loop_safe_mode": true,
|
||||||
|
"sensitive_action_safe_mode": false
|
||||||
},
|
},
|
||||||
"marketplace_listing": null
|
"marketplace_listing": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
import { PlayIcon, StopIcon } from "@phosphor-icons/react";
|
import { CircleNotchIcon, PlayIcon, StopIcon } from "@phosphor-icons/react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
||||||
import { useRunGraph } from "./useRunGraph";
|
import { useRunGraph } from "./useRunGraph";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
||||||
const {
|
const {
|
||||||
@@ -24,6 +25,31 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
|||||||
useShallow((state) => state.isGraphRunning),
|
useShallow((state) => state.isGraphRunning),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLoading = isExecutingGraph || isTerminatingGraph || isSaving;
|
||||||
|
|
||||||
|
// Determine which icon to show with proper animation
|
||||||
|
const renderIcon = () => {
|
||||||
|
const iconClass = cn(
|
||||||
|
"size-4 transition-transform duration-200 ease-out",
|
||||||
|
!isLoading && "group-hover:scale-110",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<CircleNotchIcon
|
||||||
|
className={cn(iconClass, "animate-spin")}
|
||||||
|
weight="bold"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGraphRunning) {
|
||||||
|
return <StopIcon className={iconClass} weight="fill" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PlayIcon className={iconClass} weight="fill" />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -33,18 +59,18 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
|||||||
variant={isGraphRunning ? "destructive" : "primary"}
|
variant={isGraphRunning ? "destructive" : "primary"}
|
||||||
data-id={isGraphRunning ? "stop-graph-button" : "run-graph-button"}
|
data-id={isGraphRunning ? "stop-graph-button" : "run-graph-button"}
|
||||||
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
|
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
|
||||||
disabled={!flowID || isExecutingGraph || isTerminatingGraph}
|
disabled={!flowID || isLoading}
|
||||||
loading={isExecutingGraph || isTerminatingGraph || isSaving}
|
className="group"
|
||||||
>
|
>
|
||||||
{!isGraphRunning ? (
|
{renderIcon()}
|
||||||
<PlayIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<StopIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{isGraphRunning ? "Stop agent" : "Run agent"}
|
{isLoading
|
||||||
|
? "Processing..."
|
||||||
|
: isGraphRunning
|
||||||
|
? "Stop agent"
|
||||||
|
: "Run agent"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<RunInputDialog
|
<RunInputDialog
|
||||||
|
|||||||
@@ -61,10 +61,14 @@ export const RunInputDialog = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
set: setIsOpen,
|
set: setIsOpen,
|
||||||
}}
|
}}
|
||||||
styling={{ maxWidth: "600px", minWidth: "600px" }}
|
styling={{ maxWidth: "700px", minWidth: "700px" }}
|
||||||
>
|
>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<div className="space-y-6 p-1" data-id="run-input-dialog-content">
|
<div
|
||||||
|
className="grid grid-cols-[1fr_auto] gap-10 p-1"
|
||||||
|
data-id="run-input-dialog-content"
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
{/* Credentials Section */}
|
{/* Credentials Section */}
|
||||||
{hasCredentials() && credentialFields.length > 0 && (
|
{hasCredentials() && credentialFields.length > 0 && (
|
||||||
<div data-id="run-input-credentials-section">
|
<div data-id="run-input-credentials-section">
|
||||||
@@ -107,17 +111,17 @@ export const RunInputDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action Button */}
|
|
||||||
<div
|
<div
|
||||||
className="flex justify-end pt-2"
|
className="flex flex-col items-end justify-start"
|
||||||
data-id="run-input-actions-section"
|
data-id="run-input-actions-section"
|
||||||
>
|
>
|
||||||
{purpose === "run" && (
|
{purpose === "run" && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className="group h-fit min-w-0 gap-2"
|
className="group h-fit min-w-0 gap-2 px-10"
|
||||||
onClick={handleManualRun}
|
onClick={handleManualRun}
|
||||||
loading={isExecutingGraph}
|
loading={isExecutingGraph}
|
||||||
data-id="run-input-manual-run-button"
|
data-id="run-input-manual-run-button"
|
||||||
@@ -132,7 +136,7 @@ export const RunInputDialog = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className="group h-fit min-w-0 gap-2"
|
className="group h-fit min-w-0 gap-2 px-10"
|
||||||
onClick={() => setOpenCronSchedulerDialog(true)}
|
onClick={() => setOpenCronSchedulerDialog(true)}
|
||||||
data-id="run-input-schedule-button"
|
data-id="run-input-schedule-button"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,51 +18,47 @@ interface Props {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FloatingSafeModeToggle({
|
interface SafeModeButtonProps {
|
||||||
graph,
|
isEnabled: boolean;
|
||||||
className,
|
label: string;
|
||||||
fullWidth = false,
|
tooltipEnabled: string;
|
||||||
}: Props) {
|
tooltipDisabled: string;
|
||||||
const {
|
onToggle: () => void;
|
||||||
currentSafeMode,
|
isPending: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SafeModeButton({
|
||||||
|
isEnabled,
|
||||||
|
label,
|
||||||
|
tooltipEnabled,
|
||||||
|
tooltipDisabled,
|
||||||
|
onToggle,
|
||||||
isPending,
|
isPending,
|
||||||
shouldShowToggle,
|
fullWidth = false,
|
||||||
isStateUndetermined,
|
}: SafeModeButtonProps) {
|
||||||
handleToggle,
|
|
||||||
} = useAgentSafeMode(graph);
|
|
||||||
|
|
||||||
if (!shouldShowToggle || isStateUndetermined || isPending) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("fixed z-50", className)}>
|
|
||||||
<Tooltip delayDuration={100}>
|
<Tooltip delayDuration={100}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={currentSafeMode! ? "primary" : "outline"}
|
variant={isEnabled ? "primary" : "outline"}
|
||||||
key={graph.id}
|
|
||||||
size="small"
|
size="small"
|
||||||
title={
|
onClick={onToggle}
|
||||||
currentSafeMode!
|
disabled={isPending}
|
||||||
? "Safe Mode: ON. Human in the loop blocks require manual review"
|
className={cn("justify-start", fullWidth ? "w-full" : "")}
|
||||||
: "Safe Mode: OFF. Human in the loop blocks proceed automatically"
|
|
||||||
}
|
|
||||||
onClick={handleToggle}
|
|
||||||
className={cn(fullWidth ? "w-full" : "")}
|
|
||||||
>
|
>
|
||||||
{currentSafeMode! ? (
|
{isEnabled ? (
|
||||||
<>
|
<>
|
||||||
<ShieldCheckIcon weight="bold" size={16} />
|
<ShieldCheckIcon weight="bold" size={16} />
|
||||||
<Text variant="body" className="text-zinc-200">
|
<Text variant="body" className="text-zinc-200">
|
||||||
Safe Mode: ON
|
{label}: ON
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ShieldIcon weight="bold" size={16} />
|
<ShieldIcon weight="bold" size={16} />
|
||||||
<Text variant="body" className="text-zinc-600">
|
<Text variant="body" className="text-zinc-600">
|
||||||
Safe Mode: OFF
|
{label}: OFF
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -71,16 +67,69 @@ export function FloatingSafeModeToggle({
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
Safe Mode: {currentSafeMode! ? "ON" : "OFF"}
|
{label}: {isEnabled ? "ON" : "OFF"}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{currentSafeMode!
|
{isEnabled ? tooltipEnabled : tooltipDisabled}
|
||||||
? "Human in the loop blocks require manual review"
|
|
||||||
: "Human in the loop blocks proceed automatically"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FloatingSafeModeToggle({
|
||||||
|
graph,
|
||||||
|
className,
|
||||||
|
fullWidth = false,
|
||||||
|
}: Props) {
|
||||||
|
const {
|
||||||
|
currentHITLSafeMode,
|
||||||
|
showHITLToggle,
|
||||||
|
isHITLStateUndetermined,
|
||||||
|
handleHITLToggle,
|
||||||
|
currentSensitiveActionSafeMode,
|
||||||
|
showSensitiveActionToggle,
|
||||||
|
handleSensitiveActionToggle,
|
||||||
|
isPending,
|
||||||
|
shouldShowToggle,
|
||||||
|
} = useAgentSafeMode(graph);
|
||||||
|
|
||||||
|
if (!shouldShowToggle || isPending) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showHITL = showHITLToggle && !isHITLStateUndetermined;
|
||||||
|
const showSensitive = showSensitiveActionToggle;
|
||||||
|
|
||||||
|
if (!showHITL && !showSensitive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("fixed z-50 flex flex-col gap-2", className)}>
|
||||||
|
{showHITL && (
|
||||||
|
<SafeModeButton
|
||||||
|
isEnabled={currentHITLSafeMode}
|
||||||
|
label="Human in the loop block approval"
|
||||||
|
tooltipEnabled="The agent will pause at human-in-the-loop blocks and wait for your approval"
|
||||||
|
tooltipDisabled="Human in the loop blocks will proceed automatically"
|
||||||
|
onToggle={handleHITLToggle}
|
||||||
|
isPending={isPending}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showSensitive && (
|
||||||
|
<SafeModeButton
|
||||||
|
isEnabled={currentSensitiveActionSafeMode}
|
||||||
|
label="Sensitive actions blocks approval"
|
||||||
|
tooltipEnabled="The agent will pause at sensitive action blocks and wait for your approval"
|
||||||
|
tooltipDisabled="Sensitive action blocks will proceed automatically"
|
||||||
|
onToggle={handleSensitiveActionToggle}
|
||||||
|
isPending={isPending}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ export const CustomControls = memo(
|
|||||||
const controls = [
|
const controls = [
|
||||||
{
|
{
|
||||||
id: "zoom-in-button",
|
id: "zoom-in-button",
|
||||||
icon: <PlusIcon className="size-4" />,
|
icon: <PlusIcon className="size-3.5 text-zinc-600" />,
|
||||||
label: "Zoom In",
|
label: "Zoom In",
|
||||||
onClick: () => zoomIn(),
|
onClick: () => zoomIn(),
|
||||||
className: "h-10 w-10 border-none",
|
className: "h-10 w-10 border-none",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "zoom-out-button",
|
id: "zoom-out-button",
|
||||||
icon: <MinusIcon className="size-4" />,
|
icon: <MinusIcon className="size-3.5 text-zinc-600" />,
|
||||||
label: "Zoom Out",
|
label: "Zoom Out",
|
||||||
onClick: () => zoomOut(),
|
onClick: () => zoomOut(),
|
||||||
className: "h-10 w-10 border-none",
|
className: "h-10 w-10 border-none",
|
||||||
@@ -68,9 +68,9 @@ export const CustomControls = memo(
|
|||||||
{
|
{
|
||||||
id: "tutorial-button",
|
id: "tutorial-button",
|
||||||
icon: isTutorialLoading ? (
|
icon: isTutorialLoading ? (
|
||||||
<CircleNotchIcon className="size-4 animate-spin" />
|
<CircleNotchIcon className="size-3.5 animate-spin text-zinc-600" />
|
||||||
) : (
|
) : (
|
||||||
<ChalkboardIcon className="size-4" />
|
<ChalkboardIcon className="size-3.5 text-zinc-600" />
|
||||||
),
|
),
|
||||||
label: isTutorialLoading ? "Loading Tutorial..." : "Start Tutorial",
|
label: isTutorialLoading ? "Loading Tutorial..." : "Start Tutorial",
|
||||||
onClick: handleTutorialClick,
|
onClick: handleTutorialClick,
|
||||||
@@ -79,7 +79,7 @@ export const CustomControls = memo(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fit-view-button",
|
id: "fit-view-button",
|
||||||
icon: <FrameCornersIcon className="size-4" />,
|
icon: <FrameCornersIcon className="size-3.5 text-zinc-600" />,
|
||||||
label: "Fit View",
|
label: "Fit View",
|
||||||
onClick: () => fitView({ padding: 0.2, duration: 800, maxZoom: 1 }),
|
onClick: () => fitView({ padding: 0.2, duration: 800, maxZoom: 1 }),
|
||||||
className: "h-10 w-10 border-none",
|
className: "h-10 w-10 border-none",
|
||||||
@@ -87,9 +87,9 @@ export const CustomControls = memo(
|
|||||||
{
|
{
|
||||||
id: "lock-button",
|
id: "lock-button",
|
||||||
icon: !isLocked ? (
|
icon: !isLocked ? (
|
||||||
<LockOpenIcon className="size-4" />
|
<LockOpenIcon className="size-3.5 text-zinc-600" />
|
||||||
) : (
|
) : (
|
||||||
<LockIcon className="size-4" />
|
<LockIcon className="size-3.5 text-zinc-600" />
|
||||||
),
|
),
|
||||||
label: "Toggle Lock",
|
label: "Toggle Lock",
|
||||||
onClick: () => setIsLocked(!isLocked),
|
onClick: () => setIsLocked(!isLocked),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export type CustomEdgeData = {
|
|||||||
beadUp?: number;
|
beadUp?: number;
|
||||||
beadDown?: number;
|
beadDown?: number;
|
||||||
beadData?: Map<string, NodeExecutionResult["status"]>;
|
beadData?: Map<string, NodeExecutionResult["status"]>;
|
||||||
|
edgeColorClass?: string;
|
||||||
|
edgeHexColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomEdge = XYEdge<CustomEdgeData, "custom">;
|
export type CustomEdge = XYEdge<CustomEdgeData, "custom">;
|
||||||
@@ -36,7 +38,6 @@ const CustomEdge = ({
|
|||||||
selected,
|
selected,
|
||||||
}: EdgeProps<CustomEdge>) => {
|
}: EdgeProps<CustomEdge>) => {
|
||||||
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
||||||
// Subscribe to the brokenEdgeIDs map and check if this edge is broken across any node
|
|
||||||
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
|
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ const CustomEdge = ({
|
|||||||
const isStatic = data?.isStatic ?? false;
|
const isStatic = data?.isStatic ?? false;
|
||||||
const beadUp = data?.beadUp ?? 0;
|
const beadUp = data?.beadUp ?? 0;
|
||||||
const beadDown = data?.beadDown ?? 0;
|
const beadDown = data?.beadDown ?? 0;
|
||||||
|
const edgeColorClass = data?.edgeColorClass;
|
||||||
|
|
||||||
const handleRemoveEdge = () => {
|
const handleRemoveEdge = () => {
|
||||||
removeConnection(id);
|
removeConnection(id);
|
||||||
@@ -70,6 +72,8 @@ const CustomEdge = ({
|
|||||||
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
||||||
: selected
|
: selected
|
||||||
? "stroke-zinc-800"
|
? "stroke-zinc-800"
|
||||||
|
: edgeColorClass
|
||||||
|
? cn(edgeColorClass, "opacity-70 hover:opacity-100")
|
||||||
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useCallback } from "react";
|
|||||||
import { useNodeStore } from "../../../stores/nodeStore";
|
import { useNodeStore } from "../../../stores/nodeStore";
|
||||||
import { useHistoryStore } from "../../../stores/historyStore";
|
import { useHistoryStore } from "../../../stores/historyStore";
|
||||||
import { CustomEdge } from "./CustomEdge";
|
import { CustomEdge } from "./CustomEdge";
|
||||||
|
import { getEdgeColorFromOutputType } from "../nodes/helpers";
|
||||||
|
|
||||||
export const useCustomEdge = () => {
|
export const useCustomEdge = () => {
|
||||||
const edges = useEdgeStore((s) => s.edges);
|
const edges = useEdgeStore((s) => s.edges);
|
||||||
@@ -34,8 +35,13 @@ export const useCustomEdge = () => {
|
|||||||
if (exists) return;
|
if (exists) return;
|
||||||
|
|
||||||
const nodes = useNodeStore.getState().nodes;
|
const nodes = useNodeStore.getState().nodes;
|
||||||
const isStatic = nodes.find((n) => n.id === conn.source)?.data
|
const sourceNode = nodes.find((n) => n.id === conn.source);
|
||||||
?.staticOutput;
|
const isStatic = sourceNode?.data?.staticOutput;
|
||||||
|
|
||||||
|
const { colorClass, hexColor } = getEdgeColorFromOutputType(
|
||||||
|
sourceNode?.data?.outputSchema,
|
||||||
|
conn.sourceHandle,
|
||||||
|
);
|
||||||
|
|
||||||
addEdge({
|
addEdge({
|
||||||
source: conn.source,
|
source: conn.source,
|
||||||
@@ -44,6 +50,8 @@ export const useCustomEdge = () => {
|
|||||||
targetHandle: conn.targetHandle,
|
targetHandle: conn.targetHandle,
|
||||||
data: {
|
data: {
|
||||||
isStatic,
|
isStatic,
|
||||||
|
edgeColorClass: colorClass,
|
||||||
|
edgeHexColor: hexColor,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -187,3 +187,38 @@ export const getTypeDisplayInfo = (schema: any) => {
|
|||||||
hexColor,
|
hexColor,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getEdgeColorFromOutputType(
|
||||||
|
outputSchema: RJSFSchema | undefined,
|
||||||
|
sourceHandle: string,
|
||||||
|
): { colorClass: string; hexColor: string } {
|
||||||
|
const defaultColor = {
|
||||||
|
colorClass: "stroke-zinc-500/50",
|
||||||
|
hexColor: "#6b7280",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!outputSchema?.properties) return defaultColor;
|
||||||
|
|
||||||
|
const properties = outputSchema.properties as Record<string, unknown>;
|
||||||
|
const handleParts = sourceHandle.split("_#_");
|
||||||
|
let currentSchema: Record<string, unknown> = properties;
|
||||||
|
|
||||||
|
for (let i = 0; i < handleParts.length; i++) {
|
||||||
|
const part = handleParts[i];
|
||||||
|
const fieldSchema = currentSchema[part] as Record<string, unknown>;
|
||||||
|
if (!fieldSchema) return defaultColor;
|
||||||
|
|
||||||
|
if (i === handleParts.length - 1) {
|
||||||
|
const { hexColor, colorClass } = getTypeDisplayInfo(fieldSchema);
|
||||||
|
return { colorClass: colorClass.replace("!text-", "stroke-"), hexColor };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldSchema.properties) {
|
||||||
|
currentSchema = fieldSchema.properties as Record<string, unknown>;
|
||||||
|
} else {
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,32 @@
|
|||||||
// These are SVG Phosphor icons
|
type IconOptions = {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SIZE = 16;
|
||||||
|
const DEFAULT_COLOR = "#52525b"; // zinc-600
|
||||||
|
|
||||||
|
const iconPaths = {
|
||||||
|
ClickIcon: `M88,24V16a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0ZM16,104h8a8,8,0,0,0,0-16H16a8,8,0,0,0,0,16ZM124.42,39.16a8,8,0,0,0,10.74-3.58l8-16a8,8,0,0,0-14.31-7.16l-8,16A8,8,0,0,0,124.42,39.16Zm-96,81.69-16,8a8,8,0,0,0,7.16,14.31l16-8a8,8,0,1,0-7.16-14.31ZM219.31,184a16,16,0,0,1,0,22.63l-12.68,12.68a16,16,0,0,1-22.63,0L132.7,168,115,214.09c0,.1-.08.21-.13.32a15.83,15.83,0,0,1-14.6,9.59l-.79,0a15.83,15.83,0,0,1-14.41-11L32.8,52.92A16,16,0,0,1,52.92,32.8L213,85.07a16,16,0,0,1,1.41,29.8l-.32.13L168,132.69ZM208,195.31,156.69,144h0a16,16,0,0,1,4.93-26l.32-.14,45.95-17.64L48,48l52.2,159.86,17.65-46c0-.11.08-.22.13-.33a16,16,0,0,1,11.69-9.34,16.72,16.72,0,0,1,3-.28,16,16,0,0,1,11.3,4.69L195.31,208Z`,
|
||||||
|
Keyboard: `M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z`,
|
||||||
|
Drag: `M188,80a27.79,27.79,0,0,0-13.36,3.4,28,28,0,0,0-46.64-11A28,28,0,0,0,80,92v20H68a28,28,0,0,0-28,28v12a88,88,0,0,0,176,0V108A28,28,0,0,0,188,80Zm12,72a72,72,0,0,1-144,0V140a12,12,0,0,1,12-12H80v24a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V108a12,12,0,0,1,24,0Z`,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createIcon(path: string, options: IconOptions = {}): string {
|
||||||
|
const size = options.size ?? DEFAULT_SIZE;
|
||||||
|
const color = options.color ?? DEFAULT_COLOR;
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="${color}" viewBox="0 0 256 256"><path d="${path}"></path></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
ClickIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256"><path d="M88,24V16a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0ZM16,104h8a8,8,0,0,0,0-16H16a8,8,0,0,0,0,16ZM124.42,39.16a8,8,0,0,0,10.74-3.58l8-16a8,8,0,0,0-14.31-7.16l-8,16A8,8,0,0,0,124.42,39.16Zm-96,81.69-16,8a8,8,0,0,0,7.16,14.31l16-8a8,8,0,1,0-7.16-14.31ZM219.31,184a16,16,0,0,1,0,22.63l-12.68,12.68a16,16,0,0,1-22.63,0L132.7,168,115,214.09c0,.1-.08.21-.13.32a15.83,15.83,0,0,1-14.6,9.59l-.79,0a15.83,15.83,0,0,1-14.41-11L32.8,52.92A16,16,0,0,1,52.92,32.8L213,85.07a16,16,0,0,1,1.41,29.8l-.32.13L168,132.69ZM208,195.31,156.69,144h0a16,16,0,0,1,4.93-26l.32-.14,45.95-17.64L48,48l52.2,159.86,17.65-46c0-.11.08-.22.13-.33a16,16,0,0,1,11.69-9.34,16.72,16.72,0,0,1,3-.28,16,16,0,0,1,11.3,4.69L195.31,208Z"></path></svg>`,
|
ClickIcon: createIcon(iconPaths.ClickIcon),
|
||||||
Keyboard: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256"><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z"></path></svg>`,
|
Keyboard: createIcon(iconPaths.Keyboard),
|
||||||
Drag: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256"><path d="M188,80a27.79,27.79,0,0,0-13.36,3.4,28,28,0,0,0-46.64-11A28,28,0,0,0,80,92v20H68a28,28,0,0,0-28,28v12a88,88,0,0,0,176,0V108A28,28,0,0,0,188,80Zm12,72a72,72,0,0,1-144,0V140a12,12,0,0,1,12-12H80v24a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V108a12,12,0,0,1,24,0Z"></path></svg>`,
|
Drag: createIcon(iconPaths.Drag),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getIcon(
|
||||||
|
name: keyof typeof iconPaths,
|
||||||
|
options?: IconOptions,
|
||||||
|
): string {
|
||||||
|
return createIcon(iconPaths[name], options);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import { useNodeStore } from "../../../stores/nodeStore";
|
import { useNodeStore } from "../../../stores/nodeStore";
|
||||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||||
|
import { useTutorialStore } from "../../../stores/tutorialStore";
|
||||||
|
|
||||||
let isTutorialLoading = false;
|
let isTutorialLoading = false;
|
||||||
let tutorialLoadingCallback: ((loading: boolean) => void) | null = null;
|
let tutorialLoadingCallback: ((loading: boolean) => void) | null = null;
|
||||||
@@ -60,12 +61,14 @@ export const startTutorial = async () => {
|
|||||||
handleTutorialComplete();
|
handleTutorialComplete();
|
||||||
removeTutorialStyles();
|
removeTutorialStyles();
|
||||||
clearPrefetchedBlocks();
|
clearPrefetchedBlocks();
|
||||||
|
useTutorialStore.getState().setIsTutorialRunning(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
tour.on("cancel", () => {
|
tour.on("cancel", () => {
|
||||||
handleTutorialCancel(tour);
|
handleTutorialCancel(tour);
|
||||||
removeTutorialStyles();
|
removeTutorialStyles();
|
||||||
clearPrefetchedBlocks();
|
clearPrefetchedBlocks();
|
||||||
|
useTutorialStore.getState().setIsTutorialRunning(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const step of tour.steps) {
|
for (const step of tour.steps) {
|
||||||
|
|||||||
@@ -267,23 +267,34 @@ export function extractCredentialsNeeded(
|
|||||||
| undefined;
|
| undefined;
|
||||||
if (missingCreds && Object.keys(missingCreds).length > 0) {
|
if (missingCreds && Object.keys(missingCreds).length > 0) {
|
||||||
const agentName = (setupInfo?.agent_name as string) || "this block";
|
const agentName = (setupInfo?.agent_name as string) || "this block";
|
||||||
const credentials = Object.values(missingCreds).map((credInfo) => ({
|
const credentials = Object.values(missingCreds).map((credInfo) => {
|
||||||
|
// Normalize to array at boundary - prefer 'types' array, fall back to single 'type'
|
||||||
|
const typesArray = credInfo.types as
|
||||||
|
| Array<"api_key" | "oauth2" | "user_password" | "host_scoped">
|
||||||
|
| undefined;
|
||||||
|
const singleType =
|
||||||
|
(credInfo.type as
|
||||||
|
| "api_key"
|
||||||
|
| "oauth2"
|
||||||
|
| "user_password"
|
||||||
|
| "host_scoped"
|
||||||
|
| undefined) || "api_key";
|
||||||
|
const credentialTypes =
|
||||||
|
typesArray && typesArray.length > 0 ? typesArray : [singleType];
|
||||||
|
|
||||||
|
return {
|
||||||
provider: (credInfo.provider as string) || "unknown",
|
provider: (credInfo.provider as string) || "unknown",
|
||||||
providerName:
|
providerName:
|
||||||
(credInfo.provider_name as string) ||
|
(credInfo.provider_name as string) ||
|
||||||
(credInfo.provider as string) ||
|
(credInfo.provider as string) ||
|
||||||
"Unknown Provider",
|
"Unknown Provider",
|
||||||
credentialType:
|
credentialTypes,
|
||||||
(credInfo.type as
|
|
||||||
| "api_key"
|
|
||||||
| "oauth2"
|
|
||||||
| "user_password"
|
|
||||||
| "host_scoped") || "api_key",
|
|
||||||
title:
|
title:
|
||||||
(credInfo.title as string) ||
|
(credInfo.title as string) ||
|
||||||
`${(credInfo.provider_name as string) || (credInfo.provider as string)} credentials`,
|
`${(credInfo.provider_name as string) || (credInfo.provider as string)} credentials`,
|
||||||
scopes: credInfo.scopes as string[] | undefined,
|
scopes: credInfo.scopes as string[] | undefined,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
type: "credentials_needed",
|
type: "credentials_needed",
|
||||||
toolName,
|
toolName,
|
||||||
@@ -358,11 +369,14 @@ export function extractInputsNeeded(
|
|||||||
credentials.forEach((cred) => {
|
credentials.forEach((cred) => {
|
||||||
const id = cred.id as string;
|
const id = cred.id as string;
|
||||||
if (id) {
|
if (id) {
|
||||||
|
const credentialTypes = Array.isArray(cred.types)
|
||||||
|
? cred.types
|
||||||
|
: [(cred.type as string) || "api_key"];
|
||||||
credentialsSchema[id] = {
|
credentialsSchema[id] = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {},
|
properties: {},
|
||||||
credentials_provider: [cred.provider as string],
|
credentials_provider: [cred.provider as string],
|
||||||
credentials_types: [(cred.type as string) || "api_key"],
|
credentials_types: credentialTypes,
|
||||||
credentials_scopes: cred.scopes as string[] | undefined,
|
credentials_scopes: cred.scopes as string[] | undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { useChatCredentialsSetup } from "./useChatCredentialsSetup";
|
|||||||
export interface CredentialInfo {
|
export interface CredentialInfo {
|
||||||
provider: string;
|
provider: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
credentialType: "api_key" | "oauth2" | "user_password" | "host_scoped";
|
credentialTypes: Array<
|
||||||
|
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
||||||
|
>;
|
||||||
title: string;
|
title: string;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
}
|
}
|
||||||
@@ -30,7 +32,7 @@ function createSchemaFromCredentialInfo(
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {},
|
properties: {},
|
||||||
credentials_provider: [credential.provider],
|
credentials_provider: [credential.provider],
|
||||||
credentials_types: [credential.credentialType],
|
credentials_types: credential.credentialTypes,
|
||||||
credentials_scopes: credential.scopes,
|
credentials_scopes: credential.scopes,
|
||||||
discriminator: undefined,
|
discriminator: undefined,
|
||||||
discriminator_mapping: undefined,
|
discriminator_mapping: undefined,
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export type ChatMessageData =
|
|||||||
credentials: Array<{
|
credentials: Array<{
|
||||||
provider: string;
|
provider: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
credentialType: "api_key" | "oauth2" | "user_password" | "host_scoped";
|
credentialTypes: Array<
|
||||||
|
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
||||||
|
>;
|
||||||
title: string;
|
title: string;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -31,10 +31,18 @@ export function AgentSettingsModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
const {
|
||||||
useAgentSafeMode(agent);
|
currentHITLSafeMode,
|
||||||
|
showHITLToggle,
|
||||||
|
handleHITLToggle,
|
||||||
|
currentSensitiveActionSafeMode,
|
||||||
|
showSensitiveActionToggle,
|
||||||
|
handleSensitiveActionToggle,
|
||||||
|
isPending,
|
||||||
|
shouldShowToggle,
|
||||||
|
} = useAgentSafeMode(agent);
|
||||||
|
|
||||||
if (!hasHITLBlocks) return null;
|
if (!shouldShowToggle) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -57,23 +65,48 @@ export function AgentSettingsModal({
|
|||||||
)}
|
)}
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{showHITLToggle && (
|
||||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
<div className="flex w-full items-start justify-between gap-4">
|
<div className="flex w-full items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Text variant="large-semibold">Require human approval</Text>
|
<Text variant="large-semibold">
|
||||||
|
Human-in-the-loop approval
|
||||||
|
</Text>
|
||||||
<Text variant="large" className="mt-1 text-zinc-900">
|
<Text variant="large" className="mt-1 text-zinc-900">
|
||||||
The agent will pause and wait for your review before
|
The agent will pause at human-in-the-loop blocks and wait
|
||||||
continuing
|
for your review before continuing
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={currentSafeMode || false}
|
checked={currentHITLSafeMode || false}
|
||||||
onCheckedChange={handleToggle}
|
onCheckedChange={handleHITLToggle}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{showSensitiveActionToggle && (
|
||||||
|
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
|
<div className="flex w-full items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Text variant="large-semibold">
|
||||||
|
Sensitive action approval
|
||||||
|
</Text>
|
||||||
|
<Text variant="large" className="mt-1 text-zinc-900">
|
||||||
|
The agent will pause at sensitive action blocks and wait for
|
||||||
|
your review before continuing
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={currentSensitiveActionSafeMode}
|
||||||
|
onCheckedChange={handleSensitiveActionToggle}
|
||||||
|
disabled={isPending}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -5,48 +5,112 @@ import { Graph } from "@/lib/autogpt-server-api/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ShieldCheckIcon, ShieldIcon } from "@phosphor-icons/react";
|
import { ShieldCheckIcon, ShieldIcon } from "@phosphor-icons/react";
|
||||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
graph: GraphModel | LibraryAgent | Graph;
|
graph: GraphModel | LibraryAgent | Graph;
|
||||||
className?: string;
|
className?: string;
|
||||||
fullWidth?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SafeModeToggle({ graph }: Props) {
|
interface SafeModeIconButtonProps {
|
||||||
|
isEnabled: boolean;
|
||||||
|
label: string;
|
||||||
|
tooltipEnabled: string;
|
||||||
|
tooltipDisabled: string;
|
||||||
|
onToggle: () => void;
|
||||||
|
isPending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SafeModeIconButton({
|
||||||
|
isEnabled,
|
||||||
|
label,
|
||||||
|
tooltipEnabled,
|
||||||
|
tooltipDisabled,
|
||||||
|
onToggle,
|
||||||
|
isPending,
|
||||||
|
}: SafeModeIconButtonProps) {
|
||||||
|
return (
|
||||||
|
<Tooltip delayDuration={100}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="icon"
|
||||||
|
size="icon"
|
||||||
|
aria-label={`${label}: ${isEnabled ? "ON" : "OFF"}. ${isEnabled ? tooltipEnabled : tooltipDisabled}`}
|
||||||
|
onClick={onToggle}
|
||||||
|
disabled={isPending}
|
||||||
|
className={cn(isPending ? "opacity-0" : "opacity-100")}
|
||||||
|
>
|
||||||
|
{isEnabled ? (
|
||||||
|
<ShieldCheckIcon weight="bold" size={16} />
|
||||||
|
) : (
|
||||||
|
<ShieldIcon weight="bold" size={16} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-medium">
|
||||||
|
{label}: {isEnabled ? "ON" : "OFF"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{isEnabled ? tooltipEnabled : tooltipDisabled}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SafeModeToggle({ graph, className }: Props) {
|
||||||
const {
|
const {
|
||||||
currentSafeMode,
|
currentHITLSafeMode,
|
||||||
|
showHITLToggle,
|
||||||
|
isHITLStateUndetermined,
|
||||||
|
handleHITLToggle,
|
||||||
|
currentSensitiveActionSafeMode,
|
||||||
|
showSensitiveActionToggle,
|
||||||
|
handleSensitiveActionToggle,
|
||||||
isPending,
|
isPending,
|
||||||
shouldShowToggle,
|
shouldShowToggle,
|
||||||
isStateUndetermined,
|
|
||||||
handleToggle,
|
|
||||||
} = useAgentSafeMode(graph);
|
} = useAgentSafeMode(graph);
|
||||||
|
|
||||||
if (!shouldShowToggle || isStateUndetermined) {
|
if (!shouldShowToggle || isHITLStateUndetermined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showHITL = showHITLToggle && !isHITLStateUndetermined;
|
||||||
|
const showSensitive = showSensitiveActionToggle;
|
||||||
|
|
||||||
|
if (!showHITL && !showSensitive) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className={cn("flex gap-1", className)}>
|
||||||
variant="icon"
|
{showHITL && (
|
||||||
key={graph.id}
|
<SafeModeIconButton
|
||||||
size="icon"
|
isEnabled={currentHITLSafeMode}
|
||||||
aria-label={
|
label="Human-in-the-loop"
|
||||||
currentSafeMode!
|
tooltipEnabled="The agent will pause at human-in-the-loop blocks and wait for your approval"
|
||||||
? "Safe Mode: ON. Human in the loop blocks require manual review"
|
tooltipDisabled="Human-in-the-loop blocks will proceed automatically"
|
||||||
: "Safe Mode: OFF. Human in the loop blocks proceed automatically"
|
onToggle={handleHITLToggle}
|
||||||
}
|
isPending={isPending}
|
||||||
onClick={handleToggle}
|
/>
|
||||||
className={cn(isPending ? "opacity-0" : "opacity-100")}
|
|
||||||
>
|
|
||||||
{currentSafeMode! ? (
|
|
||||||
<>
|
|
||||||
<ShieldCheckIcon weight="bold" size={16} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ShieldIcon weight="bold" size={16} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
{showSensitive && (
|
||||||
|
<SafeModeIconButton
|
||||||
|
isEnabled={currentSensitiveActionSafeMode}
|
||||||
|
label="Sensitive actions"
|
||||||
|
tooltipEnabled="The agent will pause at sensitive action blocks and wait for your approval"
|
||||||
|
tooltipDisabled="Sensitive action blocks will proceed automatically"
|
||||||
|
onToggle={handleSensitiveActionToggle}
|
||||||
|
isPending={isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,16 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
const {
|
||||||
useAgentSafeMode(agent);
|
currentHITLSafeMode,
|
||||||
|
showHITLToggle,
|
||||||
|
handleHITLToggle,
|
||||||
|
currentSensitiveActionSafeMode,
|
||||||
|
showSensitiveActionToggle,
|
||||||
|
handleSensitiveActionToggle,
|
||||||
|
isPending,
|
||||||
|
shouldShowToggle,
|
||||||
|
} = useAgentSafeMode(agent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedViewLayout agent={agent}>
|
<SelectedViewLayout agent={agent}>
|
||||||
@@ -34,24 +42,51 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
|
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
|
||||||
{hasHITLBlocks ? (
|
{shouldShowToggle ? (
|
||||||
|
<>
|
||||||
|
{showHITLToggle && (
|
||||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
<div className="flex w-full items-start justify-between gap-4">
|
<div className="flex w-full items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Text variant="large-semibold">Require human approval</Text>
|
<Text variant="large-semibold">
|
||||||
|
Human-in-the-loop approval
|
||||||
|
</Text>
|
||||||
<Text variant="large" className="mt-1 text-zinc-900">
|
<Text variant="large" className="mt-1 text-zinc-900">
|
||||||
The agent will pause and wait for your review before
|
The agent will pause at human-in-the-loop blocks and
|
||||||
continuing
|
wait for your review before continuing
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={currentSafeMode || false}
|
checked={currentHITLSafeMode || false}
|
||||||
onCheckedChange={handleToggle}
|
onCheckedChange={handleHITLToggle}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{showSensitiveActionToggle && (
|
||||||
|
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
|
<div className="flex w-full items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Text variant="large-semibold">
|
||||||
|
Sensitive action approval
|
||||||
|
</Text>
|
||||||
|
<Text variant="large" className="mt-1 text-zinc-900">
|
||||||
|
The agent will pause at sensitive action blocks and wait
|
||||||
|
for your review before continuing
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={currentSensitiveActionSafeMode}
|
||||||
|
onCheckedChange={handleSensitiveActionToggle}
|
||||||
|
disabled={isPending}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
<Text variant="body" className="text-muted-foreground">
|
<Text variant="body" className="text-muted-foreground">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { FileInput } from "@/components/atoms/FileInput/FileInput";
|
import { FileInput } from "@/components/atoms/FileInput/FileInput";
|
||||||
import { Input } from "@/components/atoms/Input/Input";
|
import { Input } from "@/components/atoms/Input/Input";
|
||||||
|
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -120,7 +121,7 @@ export default function LibraryUploadAgentDialog() {
|
|||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-t-2 border-white"></div>
|
<LoadingSpinner size="small" className="text-white" />
|
||||||
<span>Uploading...</span>
|
<span>Uploading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import {
|
|||||||
CarouselContent,
|
CarouselContent,
|
||||||
CarouselItem,
|
CarouselItem,
|
||||||
} from "@/components/__legacy__/ui/carousel";
|
} from "@/components/__legacy__/ui/carousel";
|
||||||
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
|
|
||||||
import { StaggeredList } from "@/components/molecules/StaggeredList/StaggeredList";
|
|
||||||
import { useAgentsSection } from "./useAgentsSection";
|
import { useAgentsSection } from "./useAgentsSection";
|
||||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||||
import { StoreCard } from "../StoreCard/StoreCard";
|
import { StoreCard } from "../StoreCard/StoreCard";
|
||||||
@@ -43,14 +41,12 @@ export const AgentsSection = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<div className="w-full max-w-[1360px]">
|
<div className="w-full max-w-[1360px]">
|
||||||
<FadeIn direction="left" duration={0.5}>
|
|
||||||
<h2
|
<h2
|
||||||
style={{ marginBottom: margin }}
|
style={{ marginBottom: margin }}
|
||||||
className="font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200"
|
className="font-poppins text-lg font-semibold text-[#282828] dark:text-neutral-200"
|
||||||
>
|
>
|
||||||
{sectionTitle}
|
{sectionTitle}
|
||||||
</h2>
|
</h2>
|
||||||
</FadeIn>
|
|
||||||
{!displayedAgents || displayedAgents.length === 0 ? (
|
{!displayedAgents || displayedAgents.length === 0 ? (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||||
No agents found
|
No agents found
|
||||||
@@ -58,8 +54,8 @@ export const AgentsSection = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Mobile Carousel View */}
|
{/* Mobile Carousel View */}
|
||||||
<FadeIn direction="up" className="md:hidden">
|
|
||||||
<Carousel
|
<Carousel
|
||||||
|
className="md:hidden"
|
||||||
opts={{
|
opts={{
|
||||||
loop: true,
|
loop: true,
|
||||||
}}
|
}}
|
||||||
@@ -82,14 +78,8 @@ export const AgentsSection = ({
|
|||||||
))}
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</FadeIn>
|
|
||||||
|
|
||||||
{/* Desktop Grid View with Staggered Animation */}
|
<div className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||||
<StaggeredList
|
|
||||||
direction="up"
|
|
||||||
staggerDelay={0.08}
|
|
||||||
className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4"
|
|
||||||
>
|
|
||||||
{displayedAgents.map((agent, index) => (
|
{displayedAgents.map((agent, index) => (
|
||||||
<StoreCard
|
<StoreCard
|
||||||
key={index}
|
key={index}
|
||||||
@@ -104,7 +94,7 @@ export const AgentsSection = ({
|
|||||||
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StaggeredList>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function BecomeACreator({
|
|||||||
|
|
||||||
<PublishAgentModal
|
<PublishAgentModal
|
||||||
trigger={
|
trigger={
|
||||||
<button className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:focus-visible:ring-neutral-50 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5">
|
<button className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5">
|
||||||
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
|
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -20,18 +20,9 @@ export const CreatorCard = ({
|
|||||||
}: CreatorCardProps) => {
|
}: CreatorCardProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-[filter] duration-200 hover:brightness-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:focus-visible:ring-neutral-50`}
|
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClick();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
data-testid="creator-card"
|
data-testid="creator-card"
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`View ${creatorName}'s profile - ${agentsUploaded} agents`}
|
|
||||||
>
|
>
|
||||||
<div className="relative h-[64px] w-[64px]">
|
<div className="relative h-[64px] w-[64px]">
|
||||||
<div className="absolute inset-0 overflow-hidden rounded-full">
|
<div className="absolute inset-0 overflow-hidden rounded-full">
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
|
|
||||||
import { StaggeredList } from "@/components/molecules/StaggeredList/StaggeredList";
|
|
||||||
import { CreatorCard } from "../CreatorCard/CreatorCard";
|
import { CreatorCard } from "../CreatorCard/CreatorCard";
|
||||||
import { useFeaturedCreators } from "./useFeaturedCreators";
|
import { useFeaturedCreators } from "./useFeaturedCreators";
|
||||||
import { Creator } from "@/app/api/__generated__/models/creator";
|
import { Creator } from "@/app/api/__generated__/models/creator";
|
||||||
@@ -21,17 +19,11 @@ export const FeaturedCreators = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-center justify-center">
|
<div className="flex w-full flex-col items-center justify-center">
|
||||||
<div className="w-full max-w-[1360px]">
|
<div className="w-full max-w-[1360px]">
|
||||||
<FadeIn direction="left" duration={0.5}>
|
|
||||||
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</FadeIn>
|
|
||||||
|
|
||||||
<StaggeredList
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
direction="up"
|
|
||||||
staggerDelay={0.1}
|
|
||||||
className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4"
|
|
||||||
>
|
|
||||||
{displayedCreators.map((creator, index) => (
|
{displayedCreators.map((creator, index) => (
|
||||||
<CreatorCard
|
<CreatorCard
|
||||||
key={index}
|
key={index}
|
||||||
@@ -43,7 +35,7 @@ export const FeaturedCreators = ({
|
|||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StaggeredList>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
CarouselNext,
|
CarouselNext,
|
||||||
CarouselIndicator,
|
CarouselIndicator,
|
||||||
} from "@/components/__legacy__/ui/carousel";
|
} from "@/components/__legacy__/ui/carousel";
|
||||||
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useFeaturedSection } from "./useFeaturedSection";
|
import { useFeaturedSection } from "./useFeaturedSection";
|
||||||
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||||
@@ -26,13 +25,10 @@ export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full">
|
<section className="w-full">
|
||||||
<FadeIn direction="left" duration={0.5}>
|
|
||||||
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||||
Featured agents
|
Featured agents
|
||||||
</h2>
|
</h2>
|
||||||
</FadeIn>
|
|
||||||
|
|
||||||
<FadeIn direction="up" duration={0.6} delay={0.1}>
|
|
||||||
<Carousel
|
<Carousel
|
||||||
opts={{
|
opts={{
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -63,7 +59,6 @@ export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
|
|||||||
<CarouselNext afterClick={handleNextSlide} />
|
<CarouselNext afterClick={handleNextSlide} />
|
||||||
</div>
|
</div>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</FadeIn>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FilterChip } from "@/components/atoms/FilterChip/FilterChip";
|
import { Badge } from "@/components/__legacy__/ui/badge";
|
||||||
import { useFilterChips } from "./useFilterChips";
|
import { useFilterChips } from "./useFilterChips";
|
||||||
|
|
||||||
interface FilterChipsProps {
|
interface FilterChipsProps {
|
||||||
@@ -9,6 +9,8 @@ interface FilterChipsProps {
|
|||||||
multiSelect?: boolean;
|
multiSelect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some flaws in its logic
|
||||||
|
// FRONTEND-TODO : This needs to be fixed
|
||||||
export const FilterChips = ({
|
export const FilterChips = ({
|
||||||
badges,
|
badges,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
@@ -20,20 +22,18 @@ export const FilterChips = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5">
|
||||||
className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5"
|
|
||||||
role="group"
|
|
||||||
aria-label="Filter options"
|
|
||||||
>
|
|
||||||
{badges.map((badge) => (
|
{badges.map((badge) => (
|
||||||
<FilterChip
|
<Badge
|
||||||
key={badge}
|
key={badge}
|
||||||
label={badge}
|
variant={selectedFilters.includes(badge) ? "secondary" : "outline"}
|
||||||
selected={selectedFilters.includes(badge)}
|
className="mb-2 flex cursor-pointer items-center justify-center gap-2 rounded-full border border-black/50 px-3 py-1 dark:border-white/50 lg:mb-3 lg:gap-2.5 lg:px-6 lg:py-2"
|
||||||
onClick={() => handleBadgeClick(badge)}
|
onClick={() => handleBadgeClick(badge)}
|
||||||
size="lg"
|
>
|
||||||
className="mb-2 lg:mb-3"
|
<div className="text-sm font-light tracking-tight text-[#474747] dark:text-[#e0e0e0] lg:text-xl lg:font-medium lg:leading-9">
|
||||||
/>
|
{badge}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
|
|
||||||
import { FilterChips } from "../FilterChips/FilterChips";
|
import { FilterChips } from "../FilterChips/FilterChips";
|
||||||
import { SearchBar } from "../SearchBar/SearchBar";
|
import { SearchBar } from "../SearchBar/SearchBar";
|
||||||
import { useHeroSection } from "./useHeroSection";
|
import { useHeroSection } from "./useHeroSection";
|
||||||
@@ -10,7 +9,6 @@ export const HeroSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
|
<div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
|
||||||
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">
|
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">
|
||||||
<FadeIn direction="down" duration={0.6} delay={0}>
|
|
||||||
<div className="mb-4 text-center md:mb-8">
|
<div className="mb-4 text-center md:mb-8">
|
||||||
<h1 className="text-center">
|
<h1 className="text-center">
|
||||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
|
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
|
||||||
@@ -28,18 +26,13 @@ export const HeroSection = () => {
|
|||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
|
||||||
<FadeIn direction="up" duration={0.6} delay={0.15}>
|
|
||||||
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
|
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
|
||||||
Bringing you AI agents designed by thinkers from around the world
|
Bringing you AI agents designed by thinkers from around the world
|
||||||
</h3>
|
</h3>
|
||||||
</FadeIn>
|
|
||||||
<FadeIn direction="up" duration={0.5} delay={0.3}>
|
|
||||||
<div className="mb-4 flex justify-center sm:mb-5">
|
<div className="mb-4 flex justify-center sm:mb-5">
|
||||||
<SearchBar height="h-[74px]" />
|
<SearchBar height="h-[74px]" />
|
||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
<div>
|
||||||
<FadeIn direction="up" duration={0.5} delay={0.4}>
|
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<FilterChips
|
<FilterChips
|
||||||
badges={searchTerms}
|
badges={searchTerms}
|
||||||
@@ -47,7 +40,7 @@ export const HeroSection = () => {
|
|||||||
multiSelect={false}
|
multiSelect={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Separator } from "@/components/atoms/Separator/Separator";
|
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||||
import { FadeIn } from "@/components/molecules/FadeIn/FadeIn";
|
|
||||||
import { FeaturedSection } from "../FeaturedSection/FeaturedSection";
|
import { FeaturedSection } from "../FeaturedSection/FeaturedSection";
|
||||||
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
|
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
|
||||||
import { HeroSection } from "../HeroSection/HeroSection";
|
import { HeroSection } from "../HeroSection/HeroSection";
|
||||||
@@ -55,13 +54,11 @@ export const MainMarkeplacePage = () => {
|
|||||||
<FeaturedCreators featuredCreators={featuredCreators.creators} />
|
<FeaturedCreators featuredCreators={featuredCreators.creators} />
|
||||||
)}
|
)}
|
||||||
<Separator className="mb-[25px] mt-[60px]" />
|
<Separator className="mb-[25px] mt-[60px]" />
|
||||||
<FadeIn direction="up" duration={0.6}>
|
|
||||||
<BecomeACreator
|
<BecomeACreator
|
||||||
title="Become a Creator"
|
title="Become a Creator"
|
||||||
description="Join our ever-growing community of hackers and tinkerers"
|
description="Join our ever-growing community of hackers and tinkerers"
|
||||||
buttonText="Become a Creator"
|
buttonText="Become a Creator"
|
||||||
/>
|
/>
|
||||||
</FadeIn>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ interface SearchBarProps {
|
|||||||
export const SearchBar = ({
|
export const SearchBar = ({
|
||||||
placeholder = 'Search for tasks like "optimise SEO"',
|
placeholder = 'Search for tasks like "optimise SEO"',
|
||||||
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
|
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
|
||||||
iconColor = "text-neutral-500 dark:text-neutral-400",
|
iconColor = "text-[#646464] dark:text-neutral-400",
|
||||||
textColor = "text-neutral-500 dark:text-neutral-200",
|
textColor = "text-[#707070] dark:text-neutral-200",
|
||||||
placeholderColor = "text-neutral-500 dark:text-neutral-400",
|
placeholderColor = "text-[#707070] dark:text-neutral-400",
|
||||||
width = "w-9/10 lg:w-[56.25rem]",
|
width = "w-9/10 lg:w-[56.25rem]",
|
||||||
height = "h-[60px]",
|
height = "h-[60px]",
|
||||||
}: SearchBarProps) => {
|
}: SearchBarProps) => {
|
||||||
@@ -32,13 +32,10 @@ export const SearchBar = ({
|
|||||||
>
|
>
|
||||||
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
|
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="text"
|
||||||
name="search"
|
|
||||||
autoComplete="off"
|
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
aria-label="Search for AI agents"
|
|
||||||
className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`}
|
className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`}
|
||||||
data-testid="store-search-input"
|
data-testid="store-search-input"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,25 +1,10 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Star } from "@phosphor-icons/react";
|
import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
|
||||||
import Avatar, {
|
import Avatar, {
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from "@/components/atoms/Avatar/Avatar";
|
} from "@/components/atoms/Avatar/Avatar";
|
||||||
|
|
||||||
function StarRating({ rating }: { rating: number }) {
|
|
||||||
const stars = [];
|
|
||||||
const clampedRating = Math.max(0, Math.min(5, rating));
|
|
||||||
for (let i = 1; i <= 5; i++) {
|
|
||||||
stars.push(
|
|
||||||
<Star
|
|
||||||
key={i}
|
|
||||||
weight={i <= clampedRating ? "fill" : "regular"}
|
|
||||||
className="h-4 w-4 text-neutral-900 dark:text-yellow-500"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <>{stars}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StoreCardProps {
|
interface StoreCardProps {
|
||||||
agentName: string;
|
agentName: string;
|
||||||
agentImage: string;
|
agentImage: string;
|
||||||
@@ -49,7 +34,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-background transition-shadow duration-300 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:hover:shadow-gray-700 dark:focus-visible:ring-neutral-50"
|
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-background transition-all duration-300 hover:shadow-lg dark:hover:shadow-gray-700"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
data-testid="store-card"
|
data-testid="store-card"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -91,7 +76,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
|||||||
<div className="mt-3 flex w-full flex-1 flex-col px-4">
|
<div className="mt-3 flex w-full flex-1 flex-col px-4">
|
||||||
{/* Second Section: Agent Name and Creator Name */}
|
{/* Second Section: Agent Name and Creator Name */}
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-neutral-800 dark:text-neutral-100">
|
<h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-[#272727] dark:text-neutral-100">
|
||||||
{agentName}
|
{agentName}
|
||||||
</h3>
|
</h3>
|
||||||
{!hideAvatar && creatorName && (
|
{!hideAvatar && creatorName && (
|
||||||
@@ -122,11 +107,11 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
|||||||
{rating.toFixed(1)}
|
{rating.toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-0.5"
|
className="inline-flex items-center"
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
|
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
|
||||||
>
|
>
|
||||||
<StarRating rating={rating} />
|
{StarRatingIcons(rating)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6383,6 +6383,11 @@
|
|||||||
"title": "Has Human In The Loop",
|
"title": "Has Human In The Loop",
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
|
"has_sensitive_action": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Has Sensitive Action",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
"trigger_setup_info": {
|
"trigger_setup_info": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||||
@@ -6399,6 +6404,7 @@
|
|||||||
"output_schema",
|
"output_schema",
|
||||||
"has_external_trigger",
|
"has_external_trigger",
|
||||||
"has_human_in_the_loop",
|
"has_human_in_the_loop",
|
||||||
|
"has_sensitive_action",
|
||||||
"trigger_setup_info"
|
"trigger_setup_info"
|
||||||
],
|
],
|
||||||
"title": "BaseGraph"
|
"title": "BaseGraph"
|
||||||
@@ -7629,6 +7635,11 @@
|
|||||||
"title": "Has Human In The Loop",
|
"title": "Has Human In The Loop",
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
|
"has_sensitive_action": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Has Sensitive Action",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
"trigger_setup_info": {
|
"trigger_setup_info": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||||
@@ -7652,6 +7663,7 @@
|
|||||||
"output_schema",
|
"output_schema",
|
||||||
"has_external_trigger",
|
"has_external_trigger",
|
||||||
"has_human_in_the_loop",
|
"has_human_in_the_loop",
|
||||||
|
"has_sensitive_action",
|
||||||
"trigger_setup_info",
|
"trigger_setup_info",
|
||||||
"credentials_input_schema"
|
"credentials_input_schema"
|
||||||
],
|
],
|
||||||
@@ -7730,6 +7742,11 @@
|
|||||||
"title": "Has Human In The Loop",
|
"title": "Has Human In The Loop",
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
|
"has_sensitive_action": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Has Sensitive Action",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
"trigger_setup_info": {
|
"trigger_setup_info": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||||
@@ -7754,6 +7771,7 @@
|
|||||||
"output_schema",
|
"output_schema",
|
||||||
"has_external_trigger",
|
"has_external_trigger",
|
||||||
"has_human_in_the_loop",
|
"has_human_in_the_loop",
|
||||||
|
"has_sensitive_action",
|
||||||
"trigger_setup_info",
|
"trigger_setup_info",
|
||||||
"credentials_input_schema"
|
"credentials_input_schema"
|
||||||
],
|
],
|
||||||
@@ -7762,8 +7780,14 @@
|
|||||||
"GraphSettings": {
|
"GraphSettings": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"human_in_the_loop_safe_mode": {
|
"human_in_the_loop_safe_mode": {
|
||||||
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
|
"type": "boolean",
|
||||||
"title": "Human In The Loop Safe Mode"
|
"title": "Human In The Loop Safe Mode",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"sensitive_action_safe_mode": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Sensitive Action Safe Mode",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -7921,6 +7945,16 @@
|
|||||||
"title": "Has External Trigger",
|
"title": "Has External Trigger",
|
||||||
"description": "Whether the agent has an external trigger (e.g. webhook) node"
|
"description": "Whether the agent has an external trigger (e.g. webhook) node"
|
||||||
},
|
},
|
||||||
|
"has_human_in_the_loop": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Has Human In The Loop",
|
||||||
|
"description": "Whether the agent has human-in-the-loop blocks"
|
||||||
|
},
|
||||||
|
"has_sensitive_action": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Has Sensitive Action",
|
||||||
|
"description": "Whether the agent has sensitive action blocks"
|
||||||
|
},
|
||||||
"trigger_setup_info": {
|
"trigger_setup_info": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
{ "$ref": "#/components/schemas/GraphTriggerInfo" },
|
||||||
@@ -7967,6 +8001,8 @@
|
|||||||
"output_schema",
|
"output_schema",
|
||||||
"credentials_input_schema",
|
"credentials_input_schema",
|
||||||
"has_external_trigger",
|
"has_external_trigger",
|
||||||
|
"has_human_in_the_loop",
|
||||||
|
"has_sensitive_action",
|
||||||
"new_output",
|
"new_output",
|
||||||
"can_access_graph",
|
"can_access_graph",
|
||||||
"is_latest_version",
|
"is_latest_version",
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { FilterChip } from "./FilterChip";
|
|
||||||
|
|
||||||
const meta: Meta<typeof FilterChip> = {
|
|
||||||
title: "Atoms/FilterChip",
|
|
||||||
component: FilterChip,
|
|
||||||
tags: ["autodocs"],
|
|
||||||
parameters: {
|
|
||||||
layout: "centered",
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
size: {
|
|
||||||
control: "select",
|
|
||||||
options: ["sm", "md", "lg"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof FilterChip>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
label: "Marketing",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Selected: Story = {
|
|
||||||
args: {
|
|
||||||
label: "Marketing",
|
|
||||||
selected: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Dismissible: Story = {
|
|
||||||
args: {
|
|
||||||
label: "Marketing",
|
|
||||||
selected: true,
|
|
||||||
dismissible: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Sizes: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<FilterChip label="Small" size="sm" />
|
|
||||||
<FilterChip label="Medium" size="md" />
|
|
||||||
<FilterChip label="Large" size="lg" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Disabled: Story = {
|
|
||||||
args: {
|
|
||||||
label: "Disabled",
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function FilterChipGroupDemo() {
|
|
||||||
const filters = [
|
|
||||||
"Marketing",
|
|
||||||
"Sales",
|
|
||||||
"Development",
|
|
||||||
"Design",
|
|
||||||
"Research",
|
|
||||||
"Analytics",
|
|
||||||
];
|
|
||||||
const [selected, setSelected] = useState<string[]>(["Marketing"]);
|
|
||||||
|
|
||||||
function handleToggle(filter: string) {
|
|
||||||
setSelected((prev) =>
|
|
||||||
prev.includes(filter)
|
|
||||||
? prev.filter((f) => f !== filter)
|
|
||||||
: [...prev, filter],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<FilterChip
|
|
||||||
key={filter}
|
|
||||||
label={filter}
|
|
||||||
selected={selected.includes(filter)}
|
|
||||||
onClick={() => handleToggle(filter)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterGroup: Story = {
|
|
||||||
render: () => <FilterChipGroupDemo />,
|
|
||||||
};
|
|
||||||
|
|
||||||
function SingleSelectDemo() {
|
|
||||||
const filters = ["All", "Featured", "Popular", "New"];
|
|
||||||
const [selected, setSelected] = useState("All");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<FilterChip
|
|
||||||
key={filter}
|
|
||||||
label={filter}
|
|
||||||
selected={selected === filter}
|
|
||||||
onClick={() => setSelected(filter)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SingleSelect: Story = {
|
|
||||||
render: () => <SingleSelectDemo />,
|
|
||||||
};
|
|
||||||
|
|
||||||
function DismissibleDemo() {
|
|
||||||
const [filters, setFilters] = useState([
|
|
||||||
"Marketing",
|
|
||||||
"Sales",
|
|
||||||
"Development",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function handleDismiss(filter: string) {
|
|
||||||
setFilters((prev) => prev.filter((f) => f !== filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<FilterChip
|
|
||||||
key={filter}
|
|
||||||
label={filter}
|
|
||||||
selected
|
|
||||||
dismissible
|
|
||||||
onDismiss={() => handleDismiss(filter)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{filters.length === 0 && (
|
|
||||||
<span className="text-neutral-500">No filters selected</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DismissibleGroup: Story = {
|
|
||||||
render: () => <DismissibleDemo />,
|
|
||||||
};
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { X } from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
type FilterChipSize = "sm" | "md" | "lg";
|
|
||||||
|
|
||||||
interface FilterChipProps {
|
|
||||||
/** The label text displayed in the chip */
|
|
||||||
label: string;
|
|
||||||
/** Whether the chip is currently selected */
|
|
||||||
selected?: boolean;
|
|
||||||
/** Callback when the chip is clicked */
|
|
||||||
onClick?: () => void;
|
|
||||||
/** Whether to show a dismiss/remove button */
|
|
||||||
dismissible?: boolean;
|
|
||||||
/** Callback when the dismiss button is clicked */
|
|
||||||
onDismiss?: () => void;
|
|
||||||
/** Size variant of the chip */
|
|
||||||
size?: FilterChipSize;
|
|
||||||
/** Whether the chip is disabled */
|
|
||||||
disabled?: boolean;
|
|
||||||
/** Additional CSS classes */
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeStyles: Record<FilterChipSize, string> = {
|
|
||||||
sm: "px-3 py-1 text-sm gap-1.5",
|
|
||||||
md: "px-4 py-1.5 text-base gap-2",
|
|
||||||
lg: "px-6 py-2 text-lg gap-2.5 lg:text-xl lg:leading-9",
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconSizes: Record<FilterChipSize, string> = {
|
|
||||||
sm: "h-3 w-3",
|
|
||||||
md: "h-4 w-4",
|
|
||||||
lg: "h-5 w-5",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A filter chip component for selecting/deselecting filter options.
|
|
||||||
* Supports single and multi-select patterns with proper accessibility.
|
|
||||||
*/
|
|
||||||
export function FilterChip({
|
|
||||||
label,
|
|
||||||
selected = false,
|
|
||||||
onClick,
|
|
||||||
dismissible = false,
|
|
||||||
onDismiss,
|
|
||||||
size = "md",
|
|
||||||
disabled = false,
|
|
||||||
className,
|
|
||||||
}: FilterChipProps) {
|
|
||||||
function handleDismiss(e: React.MouseEvent) {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDismiss?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
aria-pressed={selected}
|
|
||||||
className={cn(
|
|
||||||
// Base styles
|
|
||||||
"inline-flex items-center justify-center rounded-full border font-medium transition-colors",
|
|
||||||
// Focus styles
|
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:focus-visible:ring-neutral-50",
|
|
||||||
// Size styles
|
|
||||||
sizeStyles[size],
|
|
||||||
// State styles
|
|
||||||
selected
|
|
||||||
? "border-neutral-900 bg-neutral-100 text-neutral-800 dark:border-neutral-100 dark:bg-neutral-800 dark:text-neutral-200"
|
|
||||||
: "border-neutral-400 bg-transparent text-neutral-600 hover:bg-neutral-50 dark:border-neutral-500 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
|
||||||
// Disabled styles
|
|
||||||
disabled && "pointer-events-none opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{label}</span>
|
|
||||||
{dismissible && selected && (
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={handleDismiss}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
handleDismiss(e as unknown as React.MouseEvent);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="rounded-full p-0.5 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
|
||||||
aria-label={`Remove ${label} filter`}
|
|
||||||
>
|
|
||||||
<X className={iconSizes[size]} weight="bold" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
|
||||||
import { Separator } from "./Separator";
|
|
||||||
|
|
||||||
const meta: Meta<typeof Separator> = {
|
|
||||||
title: "Atoms/Separator",
|
|
||||||
component: Separator,
|
|
||||||
tags: ["autodocs"],
|
|
||||||
parameters: {
|
|
||||||
layout: "padded",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Separator>;
|
|
||||||
|
|
||||||
export const Horizontal: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<p className="mb-4 text-neutral-700 dark:text-neutral-300">
|
|
||||||
Content above the separator
|
|
||||||
</p>
|
|
||||||
<Separator />
|
|
||||||
<p className="mt-4 text-neutral-700 dark:text-neutral-300">
|
|
||||||
Content below the separator
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Vertical: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="flex h-16 items-center gap-4">
|
|
||||||
<span className="text-neutral-700 dark:text-neutral-300">Left</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-neutral-700 dark:text-neutral-300">Right</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithCustomStyles: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="w-full max-w-md space-y-4">
|
|
||||||
<Separator className="bg-violet-500" />
|
|
||||||
<Separator className="h-0.5 bg-gradient-to-r from-violet-500 to-blue-500" />
|
|
||||||
<Separator className="bg-neutral-400 dark:bg-neutral-600" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InSection: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="w-full max-w-md space-y-6">
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Featured Agents
|
|
||||||
</h2>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
Browse our collection of featured AI agents.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<Separator className="my-6" />
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Top Creators
|
|
||||||
</h2>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
Meet the creators behind the most popular agents.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type SeparatorOrientation = "horizontal" | "vertical";
|
|
||||||
|
|
||||||
interface SeparatorProps {
|
|
||||||
/** The orientation of the separator */
|
|
||||||
orientation?: SeparatorOrientation;
|
|
||||||
/** Whether the separator is purely decorative (true) or represents a semantic boundary (false) */
|
|
||||||
decorative?: boolean;
|
|
||||||
/** Additional CSS classes */
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A visual separator that divides content.
|
|
||||||
* Uses semantic `<hr>` for horizontal separators and a styled `<div>` for vertical.
|
|
||||||
*/
|
|
||||||
export function Separator({
|
|
||||||
orientation = "horizontal",
|
|
||||||
decorative = true,
|
|
||||||
className,
|
|
||||||
}: SeparatorProps) {
|
|
||||||
const baseStyles = "shrink-0 bg-neutral-200 dark:bg-neutral-800";
|
|
||||||
|
|
||||||
if (orientation === "horizontal") {
|
|
||||||
return (
|
|
||||||
<hr
|
|
||||||
className={cn(baseStyles, "h-px w-full border-0", className)}
|
|
||||||
aria-hidden={decorative}
|
|
||||||
role={decorative ? "none" : "separator"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(baseStyles, "h-full w-px", className)}
|
|
||||||
aria-hidden={decorative}
|
|
||||||
role={decorative ? "none" : "separator"}
|
|
||||||
aria-orientation="vertical"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
|
||||||
import { FadeIn } from "./FadeIn";
|
|
||||||
|
|
||||||
const meta: Meta<typeof FadeIn> = {
|
|
||||||
title: "Molecules/FadeIn",
|
|
||||||
component: FadeIn,
|
|
||||||
tags: ["autodocs"],
|
|
||||||
parameters: {
|
|
||||||
layout: "padded",
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
direction: {
|
|
||||||
control: "select",
|
|
||||||
options: ["up", "down", "left", "right", "none"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof FadeIn>;
|
|
||||||
|
|
||||||
const DemoCard = ({ title }: { title: string }) => (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-6 dark:bg-neutral-800">
|
|
||||||
<h3 className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
|
||||||
This card fades in with a smooth animation.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
children: <DemoCard title="Fade Up" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeDown: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "down",
|
|
||||||
children: <DemoCard title="Fade Down" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeLeft: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "left",
|
|
||||||
children: <DemoCard title="Fade Left" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeRight: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "right",
|
|
||||||
children: <DemoCard title="Fade Right" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeOnly: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "none",
|
|
||||||
children: <DemoCard title="Fade Only (No Direction)" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithDelay: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
delay: 0.5,
|
|
||||||
children: <DemoCard title="Delayed Fade (0.5s)" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SlowAnimation: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
duration: 1.5,
|
|
||||||
children: <DemoCard title="Slow Animation (1.5s)" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LargeDistance: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
distance: 60,
|
|
||||||
children: <DemoCard title="Large Distance (60px)" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MultipleElements: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<FadeIn direction="up" delay={0}>
|
|
||||||
<DemoCard title="First Card" />
|
|
||||||
</FadeIn>
|
|
||||||
<FadeIn direction="up" delay={0.1}>
|
|
||||||
<DemoCard title="Second Card" />
|
|
||||||
</FadeIn>
|
|
||||||
<FadeIn direction="up" delay={0.2}>
|
|
||||||
<DemoCard title="Third Card" />
|
|
||||||
</FadeIn>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HeroExample: Story = {
|
|
||||||
render: () => (
|
|
||||||
<div className="text-center">
|
|
||||||
<FadeIn direction="down" delay={0}>
|
|
||||||
<h1 className="mb-4 text-4xl font-bold text-neutral-900 dark:text-neutral-100">
|
|
||||||
Welcome to the Marketplace
|
|
||||||
</h1>
|
|
||||||
</FadeIn>
|
|
||||||
<FadeIn direction="up" delay={0.2}>
|
|
||||||
<p className="mb-8 text-xl text-neutral-600 dark:text-neutral-400">
|
|
||||||
Discover AI agents built by the community
|
|
||||||
</p>
|
|
||||||
</FadeIn>
|
|
||||||
<FadeIn direction="up" delay={0.4}>
|
|
||||||
<button className="rounded-full bg-violet-600 px-8 py-3 text-white hover:bg-violet-700">
|
|
||||||
Get Started
|
|
||||||
</button>
|
|
||||||
</FadeIn>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { motion, useReducedMotion, type Variants } from "framer-motion";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
type FadeDirection = "up" | "down" | "left" | "right" | "none";
|
|
||||||
|
|
||||||
interface FadeInProps {
|
|
||||||
/** Content to animate */
|
|
||||||
children: ReactNode;
|
|
||||||
/** Direction the content fades in from */
|
|
||||||
direction?: FadeDirection;
|
|
||||||
/** Distance to travel in pixels (only applies when direction is not "none") */
|
|
||||||
distance?: number;
|
|
||||||
/** Animation duration in seconds */
|
|
||||||
duration?: number;
|
|
||||||
/** Delay before animation starts in seconds */
|
|
||||||
delay?: number;
|
|
||||||
/** Whether to trigger animation when element enters viewport */
|
|
||||||
viewport?: boolean;
|
|
||||||
/** How much of element must be visible to trigger (0-1) */
|
|
||||||
viewportAmount?: number;
|
|
||||||
/** Whether animation should only trigger once */
|
|
||||||
once?: boolean;
|
|
||||||
/** Additional CSS classes */
|
|
||||||
className?: string;
|
|
||||||
/** HTML element to render as */
|
|
||||||
as?: keyof JSX.IntrinsicElements;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDirectionOffset(
|
|
||||||
direction: FadeDirection,
|
|
||||||
distance: number,
|
|
||||||
): { x: number; y: number } {
|
|
||||||
switch (direction) {
|
|
||||||
case "up":
|
|
||||||
return { x: 0, y: distance };
|
|
||||||
case "down":
|
|
||||||
return { x: 0, y: -distance };
|
|
||||||
case "left":
|
|
||||||
return { x: distance, y: 0 };
|
|
||||||
case "right":
|
|
||||||
return { x: -distance, y: 0 };
|
|
||||||
case "none":
|
|
||||||
default:
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A fade-in animation wrapper component.
|
|
||||||
* Animates children with a fade effect and optional directional slide.
|
|
||||||
* Respects user's reduced motion preferences.
|
|
||||||
*/
|
|
||||||
export function FadeIn({
|
|
||||||
children,
|
|
||||||
direction = "up",
|
|
||||||
distance = 24,
|
|
||||||
duration = 0.5,
|
|
||||||
delay = 0,
|
|
||||||
viewport = true,
|
|
||||||
viewportAmount = 0.2,
|
|
||||||
once = true,
|
|
||||||
className,
|
|
||||||
as = "div",
|
|
||||||
}: FadeInProps) {
|
|
||||||
const shouldReduceMotion = useReducedMotion();
|
|
||||||
const offset = getDirectionOffset(direction, distance);
|
|
||||||
|
|
||||||
// If user prefers reduced motion, render without animation
|
|
||||||
if (shouldReduceMotion) {
|
|
||||||
const Component = as as keyof JSX.IntrinsicElements;
|
|
||||||
return <Component className={className}>{children}</Component>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const variants: Variants = {
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
x: offset.x,
|
|
||||||
y: offset.y,
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration,
|
|
||||||
delay,
|
|
||||||
ease: [0.25, 0.1, 0.25, 1], // Custom easing for smooth feel
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const MotionComponent = motion[as as keyof typeof motion] as typeof motion.div;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MotionComponent
|
|
||||||
className={cn(className)}
|
|
||||||
initial="hidden"
|
|
||||||
animate={viewport ? undefined : "visible"}
|
|
||||||
whileInView={viewport ? "visible" : undefined}
|
|
||||||
viewport={viewport ? { once, amount: viewportAmount } : undefined}
|
|
||||||
variants={variants}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MotionComponent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
|
||||||
import { StaggeredList } from "./StaggeredList";
|
|
||||||
|
|
||||||
const meta: Meta<typeof StaggeredList> = {
|
|
||||||
title: "Molecules/StaggeredList",
|
|
||||||
component: StaggeredList,
|
|
||||||
tags: ["autodocs"],
|
|
||||||
parameters: {
|
|
||||||
layout: "padded",
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
direction: {
|
|
||||||
control: "select",
|
|
||||||
options: ["up", "down", "left", "right", "none"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof StaggeredList>;
|
|
||||||
|
|
||||||
const DemoCard = ({ title, index }: { title: string; index: number }) => (
|
|
||||||
<div className="rounded-xl bg-neutral-100 p-4 dark:bg-neutral-800">
|
|
||||||
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
Card #{index + 1} with staggered animation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = ["First Item", "Second Item", "Third Item", "Fourth Item"];
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
className: "space-y-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeDown: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "down",
|
|
||||||
className: "space-y-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeLeft: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "left",
|
|
||||||
className: "flex gap-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FadeRight: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "right",
|
|
||||||
className: "flex gap-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FastStagger: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
staggerDelay: 0.05,
|
|
||||||
className: "space-y-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SlowStagger: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
staggerDelay: 0.3,
|
|
||||||
className: "space-y-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithInitialDelay: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
initialDelay: 0.5,
|
|
||||||
className: "space-y-4",
|
|
||||||
children: items.map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GridLayout: Story = {
|
|
||||||
args: {
|
|
||||||
direction: "up",
|
|
||||||
staggerDelay: 0.08,
|
|
||||||
className: "grid grid-cols-2 gap-4 md:grid-cols-4",
|
|
||||||
children: [
|
|
||||||
...items,
|
|
||||||
"Fifth Item",
|
|
||||||
"Sixth Item",
|
|
||||||
"Seventh Item",
|
|
||||||
"Eighth Item",
|
|
||||||
].map((item, i) => <DemoCard key={i} title={item} index={i} />),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AgentCardsExample: Story = {
|
|
||||||
render: () => {
|
|
||||||
const agents = [
|
|
||||||
{ name: "SEO Optimizer", runs: 1234 },
|
|
||||||
{ name: "Content Writer", runs: 987 },
|
|
||||||
{ name: "Data Analyzer", runs: 756 },
|
|
||||||
{ name: "Code Reviewer", runs: 543 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StaggeredList
|
|
||||||
direction="up"
|
|
||||||
staggerDelay={0.1}
|
|
||||||
className="grid grid-cols-2 gap-6 md:grid-cols-4"
|
|
||||||
>
|
|
||||||
{agents.map((agent, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="rounded-2xl bg-white p-4 shadow-md dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<div className="mb-3 aspect-video rounded-xl bg-gradient-to-br from-violet-500 to-blue-500" />
|
|
||||||
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
{agent.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-neutral-500">{agent.runs} runs</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</StaggeredList>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreatorCardsExample: Story = {
|
|
||||||
render: () => {
|
|
||||||
const creators = [
|
|
||||||
{ name: "Alice", agents: 12 },
|
|
||||||
{ name: "Bob", agents: 8 },
|
|
||||||
{ name: "Charlie", agents: 15 },
|
|
||||||
{ name: "Diana", agents: 6 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const colors = [
|
|
||||||
"bg-violet-100 dark:bg-violet-900/30",
|
|
||||||
"bg-blue-100 dark:bg-blue-900/30",
|
|
||||||
"bg-green-100 dark:bg-green-900/30",
|
|
||||||
"bg-orange-100 dark:bg-orange-900/30",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StaggeredList
|
|
||||||
direction="up"
|
|
||||||
staggerDelay={0.12}
|
|
||||||
className="grid grid-cols-2 gap-6 md:grid-cols-4"
|
|
||||||
>
|
|
||||||
{creators.map((creator, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`rounded-2xl p-5 ${colors[i % colors.length]}`}
|
|
||||||
>
|
|
||||||
<div className="mb-3 h-12 w-12 rounded-full bg-neutral-300 dark:bg-neutral-700" />
|
|
||||||
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
{creator.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
{creator.agents} agents
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</StaggeredList>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { motion, useReducedMotion, type Variants } from "framer-motion";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
type StaggerDirection = "up" | "down" | "left" | "right" | "none";
|
|
||||||
|
|
||||||
interface StaggeredListProps {
|
|
||||||
/** Array of items to render with staggered animation */
|
|
||||||
children: ReactNode[];
|
|
||||||
/** Direction items animate from */
|
|
||||||
direction?: StaggerDirection;
|
|
||||||
/** Distance to travel in pixels */
|
|
||||||
distance?: number;
|
|
||||||
/** Base duration for each item's animation */
|
|
||||||
duration?: number;
|
|
||||||
/** Delay between each item's animation start */
|
|
||||||
staggerDelay?: number;
|
|
||||||
/** Initial delay before first item animates */
|
|
||||||
initialDelay?: number;
|
|
||||||
/** Whether to trigger animation when element enters viewport */
|
|
||||||
viewport?: boolean;
|
|
||||||
/** How much of container must be visible to trigger */
|
|
||||||
viewportAmount?: number;
|
|
||||||
/** Whether animation should only trigger once */
|
|
||||||
once?: boolean;
|
|
||||||
/** Additional CSS classes for the container */
|
|
||||||
className?: string;
|
|
||||||
/** Additional CSS classes for each item wrapper */
|
|
||||||
itemClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDirectionOffset(
|
|
||||||
direction: StaggerDirection,
|
|
||||||
distance: number,
|
|
||||||
): { x: number; y: number } {
|
|
||||||
switch (direction) {
|
|
||||||
case "up":
|
|
||||||
return { x: 0, y: distance };
|
|
||||||
case "down":
|
|
||||||
return { x: 0, y: -distance };
|
|
||||||
case "left":
|
|
||||||
return { x: distance, y: 0 };
|
|
||||||
case "right":
|
|
||||||
return { x: -distance, y: 0 };
|
|
||||||
case "none":
|
|
||||||
default:
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animates a list of children with staggered fade-in effects.
|
|
||||||
* Each child appears sequentially with a configurable delay.
|
|
||||||
* Respects user's reduced motion preferences.
|
|
||||||
*/
|
|
||||||
export function StaggeredList({
|
|
||||||
children,
|
|
||||||
direction = "up",
|
|
||||||
distance = 20,
|
|
||||||
duration = 0.4,
|
|
||||||
staggerDelay = 0.1,
|
|
||||||
initialDelay = 0,
|
|
||||||
viewport = true,
|
|
||||||
viewportAmount = 0.1,
|
|
||||||
once = true,
|
|
||||||
className,
|
|
||||||
itemClassName,
|
|
||||||
}: StaggeredListProps) {
|
|
||||||
const shouldReduceMotion = useReducedMotion();
|
|
||||||
const offset = getDirectionOffset(direction, distance);
|
|
||||||
|
|
||||||
// If user prefers reduced motion, render without animation
|
|
||||||
if (shouldReduceMotion) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{children.map((child, index) => (
|
|
||||||
<div key={index} className={itemClassName}>
|
|
||||||
{child}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerVariants: Variants = {
|
|
||||||
hidden: {},
|
|
||||||
visible: {
|
|
||||||
transition: {
|
|
||||||
staggerChildren: staggerDelay,
|
|
||||||
delayChildren: initialDelay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
x: offset.x,
|
|
||||||
y: offset.y,
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration,
|
|
||||||
ease: [0.25, 0.1, 0.25, 1],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className={cn(className)}
|
|
||||||
initial="hidden"
|
|
||||||
animate={viewport ? undefined : "visible"}
|
|
||||||
whileInView={viewport ? "visible" : undefined}
|
|
||||||
viewport={viewport ? { once, amount: viewportAmount } : undefined}
|
|
||||||
variants={containerVariants}
|
|
||||||
>
|
|
||||||
{children.map((child, index) => (
|
|
||||||
<motion.div key={index} className={itemClassName} variants={itemVariants}>
|
|
||||||
{child}
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -35,12 +35,13 @@ export const CredentialFieldTitle = (props: {
|
|||||||
uiOptions,
|
uiOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
const credentialProvider = toDisplayName(
|
const provider = getCredentialProviderFromSchema(
|
||||||
getCredentialProviderFromSchema(
|
|
||||||
useNodeStore.getState().getHardCodedValues(nodeId),
|
useNodeStore.getState().getHardCodedValues(nodeId),
|
||||||
schema as BlockIOCredentialsSubSchema,
|
schema as BlockIOCredentialsSubSchema,
|
||||||
) ?? "",
|
|
||||||
);
|
);
|
||||||
|
const credentialProvider = provider
|
||||||
|
? `${toDisplayName(provider)} credential`
|
||||||
|
: "credential";
|
||||||
|
|
||||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||||
showHandles: false,
|
showHandles: false,
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ function hasHITLBlocks(graph: GraphModel | LibraryAgent | Graph): boolean {
|
|||||||
if ("has_human_in_the_loop" in graph) {
|
if ("has_human_in_the_loop" in graph) {
|
||||||
return !!graph.has_human_in_the_loop;
|
return !!graph.has_human_in_the_loop;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLibraryAgent(graph)) {
|
function hasSensitiveActionBlocks(
|
||||||
return graph.settings?.human_in_the_loop_safe_mode !== null;
|
graph: GraphModel | LibraryAgent | Graph,
|
||||||
|
): boolean {
|
||||||
|
if ("has_sensitive_action" in graph) {
|
||||||
|
return !!graph.has_sensitive_action;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +44,9 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
|||||||
|
|
||||||
const graphId = getGraphId(graph);
|
const graphId = getGraphId(graph);
|
||||||
const isAgent = isLibraryAgent(graph);
|
const isAgent = isLibraryAgent(graph);
|
||||||
const shouldShowToggle = hasHITLBlocks(graph);
|
const showHITLToggle = hasHITLBlocks(graph);
|
||||||
|
const showSensitiveActionToggle = hasSensitiveActionBlocks(graph);
|
||||||
|
const shouldShowToggle = showHITLToggle || showSensitiveActionToggle;
|
||||||
|
|
||||||
const { mutateAsync: updateGraphSettings, isPending } =
|
const { mutateAsync: updateGraphSettings, isPending } =
|
||||||
usePatchV1UpdateGraphSettings();
|
usePatchV1UpdateGraphSettings();
|
||||||
@@ -56,27 +62,37 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [localSafeMode, setLocalSafeMode] = useState<boolean | null>(null);
|
const [localHITLSafeMode, setLocalHITLSafeMode] = useState<boolean>(true);
|
||||||
|
const [localSensitiveActionSafeMode, setLocalSensitiveActionSafeMode] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [isLocalStateLoaded, setIsLocalStateLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAgent && libraryAgent) {
|
if (!isAgent && libraryAgent) {
|
||||||
const backendValue = libraryAgent.settings?.human_in_the_loop_safe_mode;
|
setLocalHITLSafeMode(
|
||||||
if (backendValue !== undefined) {
|
libraryAgent.settings?.human_in_the_loop_safe_mode ?? true,
|
||||||
setLocalSafeMode(backendValue);
|
);
|
||||||
}
|
setLocalSensitiveActionSafeMode(
|
||||||
|
libraryAgent.settings?.sensitive_action_safe_mode ?? false,
|
||||||
|
);
|
||||||
|
setIsLocalStateLoaded(true);
|
||||||
}
|
}
|
||||||
}, [isAgent, libraryAgent]);
|
}, [isAgent, libraryAgent]);
|
||||||
|
|
||||||
const currentSafeMode = isAgent
|
const currentHITLSafeMode = isAgent
|
||||||
? graph.settings?.human_in_the_loop_safe_mode
|
? (graph.settings?.human_in_the_loop_safe_mode ?? true)
|
||||||
: localSafeMode;
|
: localHITLSafeMode;
|
||||||
|
|
||||||
const isStateUndetermined = isAgent
|
const currentSensitiveActionSafeMode = isAgent
|
||||||
? graph.settings?.human_in_the_loop_safe_mode == null
|
? (graph.settings?.sensitive_action_safe_mode ?? false)
|
||||||
: isLoading || localSafeMode === null;
|
: localSensitiveActionSafeMode;
|
||||||
|
|
||||||
const handleToggle = useCallback(async () => {
|
const isHITLStateUndetermined = isAgent
|
||||||
const newSafeMode = !currentSafeMode;
|
? false
|
||||||
|
: isLoading || !isLocalStateLoaded;
|
||||||
|
|
||||||
|
const handleHITLToggle = useCallback(async () => {
|
||||||
|
const newSafeMode = !currentHITLSafeMode;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateGraphSettings({
|
await updateGraphSettings({
|
||||||
@@ -85,7 +101,7 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isAgent) {
|
if (!isAgent) {
|
||||||
setLocalSafeMode(newSafeMode);
|
setLocalHITLSafeMode(newSafeMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAgent) {
|
if (isAgent) {
|
||||||
@@ -101,13 +117,99 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["v2", "executions"] });
|
queryClient.invalidateQueries({ queryKey: ["v2", "executions"] });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: `Safe mode ${newSafeMode ? "enabled" : "disabled"}`,
|
title: `HITL safe mode ${newSafeMode ? "enabled" : "disabled"}`,
|
||||||
description: newSafeMode
|
description: newSafeMode
|
||||||
? "Human-in-the-loop blocks will require manual review"
|
? "Human-in-the-loop blocks will require manual review"
|
||||||
: "Human-in-the-loop blocks will proceed automatically",
|
: "Human-in-the-loop blocks will proceed automatically",
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
handleToggleError(error, isAgent, toast);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentHITLSafeMode,
|
||||||
|
graphId,
|
||||||
|
isAgent,
|
||||||
|
graph.id,
|
||||||
|
updateGraphSettings,
|
||||||
|
queryClient,
|
||||||
|
toast,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleSensitiveActionToggle = useCallback(async () => {
|
||||||
|
const newSafeMode = !currentSensitiveActionSafeMode;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateGraphSettings({
|
||||||
|
graphId,
|
||||||
|
data: { sensitive_action_safe_mode: newSafeMode },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isAgent) {
|
||||||
|
setLocalSensitiveActionSafeMode(newSafeMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAgent) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: getGetV2GetLibraryAgentQueryOptions(graph.id.toString())
|
||||||
|
.queryKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["v1", "graphs", graphId, "executions"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["v2", "executions"] });
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: `Sensitive action safe mode ${newSafeMode ? "enabled" : "disabled"}`,
|
||||||
|
description: newSafeMode
|
||||||
|
? "Sensitive action blocks will require manual review"
|
||||||
|
: "Sensitive action blocks will proceed automatically",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleToggleError(error, isAgent, toast);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentSensitiveActionSafeMode,
|
||||||
|
graphId,
|
||||||
|
isAgent,
|
||||||
|
graph.id,
|
||||||
|
updateGraphSettings,
|
||||||
|
queryClient,
|
||||||
|
toast,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// HITL safe mode
|
||||||
|
currentHITLSafeMode,
|
||||||
|
showHITLToggle,
|
||||||
|
isHITLStateUndetermined,
|
||||||
|
handleHITLToggle,
|
||||||
|
|
||||||
|
// Sensitive action safe mode
|
||||||
|
currentSensitiveActionSafeMode,
|
||||||
|
showSensitiveActionToggle,
|
||||||
|
handleSensitiveActionToggle,
|
||||||
|
|
||||||
|
// General
|
||||||
|
isPending,
|
||||||
|
shouldShowToggle,
|
||||||
|
|
||||||
|
// Backwards compatibility
|
||||||
|
currentSafeMode: currentHITLSafeMode,
|
||||||
|
isStateUndetermined: isHITLStateUndetermined,
|
||||||
|
handleToggle: handleHITLToggle,
|
||||||
|
hasHITLBlocks: showHITLToggle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleError(
|
||||||
|
error: unknown,
|
||||||
|
isAgent: boolean,
|
||||||
|
toast: ReturnType<typeof useToast>["toast"],
|
||||||
|
) {
|
||||||
const isNotFoundError =
|
const isNotFoundError =
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
(error.message.includes("404") || error.message.includes("not found"));
|
(error.message.includes("404") || error.message.includes("not found"));
|
||||||
@@ -129,23 +231,4 @@ export function useAgentSafeMode(graph: GraphModel | LibraryAgent | Graph) {
|
|||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [
|
|
||||||
currentSafeMode,
|
|
||||||
graphId,
|
|
||||||
isAgent,
|
|
||||||
graph.id,
|
|
||||||
updateGraphSettings,
|
|
||||||
queryClient,
|
|
||||||
toast,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentSafeMode,
|
|
||||||
isPending,
|
|
||||||
shouldShowToggle,
|
|
||||||
isStateUndetermined,
|
|
||||||
handleToggle,
|
|
||||||
hasHITLBlocks: shouldShowToggle,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import isEqual from "lodash/isEqual";
|
|||||||
export function cleanNode(node: CustomNode) {
|
export function cleanNode(node: CustomNode) {
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
position: node.position,
|
// Note: position is intentionally excluded to prevent draft saves when dragging nodes
|
||||||
data: {
|
data: {
|
||||||
hardcodedValues: node.data.hardcodedValues,
|
hardcodedValues: node.data.hardcodedValues,
|
||||||
title: node.data.title,
|
title: node.data.title,
|
||||||
|
|||||||
44
docs/CLAUDE.md
Normal file
44
docs/CLAUDE.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Documentation Guidelines
|
||||||
|
|
||||||
|
## Block Documentation Manual Sections
|
||||||
|
|
||||||
|
When updating manual sections (`<!-- MANUAL: ... -->`) in block documentation files (e.g., `docs/integrations/basic.md`), follow these formats:
|
||||||
|
|
||||||
|
### How It Works Section
|
||||||
|
|
||||||
|
Provide a technical explanation of how the block functions:
|
||||||
|
- Describe the processing logic in 1-2 paragraphs
|
||||||
|
- Mention any validation, error handling, or edge cases
|
||||||
|
- Use code examples with backticks when helpful (e.g., `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```markdown
|
||||||
|
<!-- MANUAL: how_it_works -->
|
||||||
|
The block iterates through each list in the input and extends a result list with all elements from each one. It processes lists in order, so `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`.
|
||||||
|
|
||||||
|
The block includes validation to ensure each item is actually a list. If a non-list value is encountered, the block outputs an error message instead of proceeding.
|
||||||
|
<!-- END MANUAL -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Case Section
|
||||||
|
|
||||||
|
Provide 3 practical use cases in this format:
|
||||||
|
- **Bold Heading**: Short one-sentence description
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```markdown
|
||||||
|
<!-- MANUAL: use_case -->
|
||||||
|
**Paginated API Merging**: Combine results from multiple API pages into a single list for batch processing or display.
|
||||||
|
|
||||||
|
**Parallel Task Aggregation**: Merge outputs from parallel workflow branches that each produce a list of results.
|
||||||
|
|
||||||
|
**Multi-Source Data Collection**: Combine data collected from different sources (like multiple RSS feeds or API endpoints) into one unified list.
|
||||||
|
<!-- END MANUAL -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Style Guidelines
|
||||||
|
|
||||||
|
- Keep descriptions concise and action-oriented
|
||||||
|
- Focus on practical, real-world scenarios
|
||||||
|
- Use consistent terminology with other blocks
|
||||||
|
- Avoid overly technical jargon unless necessary
|
||||||
@@ -31,6 +31,7 @@ Below is a comprehensive list of all available blocks, categorized by their prim
|
|||||||
| [Agent Time Input](basic.md#agent-time-input) | Block for time input |
|
| [Agent Time Input](basic.md#agent-time-input) | Block for time input |
|
||||||
| [Agent Toggle Input](basic.md#agent-toggle-input) | Block for boolean toggle input |
|
| [Agent Toggle Input](basic.md#agent-toggle-input) | Block for boolean toggle input |
|
||||||
| [Block Installation](basic.md#block-installation) | Given a code string, this block allows the verification and installation of a block code into the system |
|
| [Block Installation](basic.md#block-installation) | Given a code string, this block allows the verification and installation of a block code into the system |
|
||||||
|
| [Concatenate Lists](basic.md#concatenate-lists) | Concatenates multiple lists into a single list |
|
||||||
| [Dictionary Is Empty](basic.md#dictionary-is-empty) | Checks if a dictionary is empty |
|
| [Dictionary Is Empty](basic.md#dictionary-is-empty) | Checks if a dictionary is empty |
|
||||||
| [File Store](basic.md#file-store) | Stores the input file in the temporary directory |
|
| [File Store](basic.md#file-store) | Stores the input file in the temporary directory |
|
||||||
| [Find In Dictionary](basic.md#find-in-dictionary) | A block that looks up a value in a dictionary, list, or object by key or index and returns the corresponding value |
|
| [Find In Dictionary](basic.md#find-in-dictionary) | A block that looks up a value in a dictionary, list, or object by key or index and returns the corresponding value |
|
||||||
|
|||||||
@@ -634,6 +634,42 @@ This enables extensibility by allowing custom blocks to be added without modifyi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Concatenate Lists
|
||||||
|
|
||||||
|
### What it is
|
||||||
|
Concatenates multiple lists into a single list. All elements from all input lists are combined in order.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
<!-- MANUAL: how_it_works -->
|
||||||
|
The block iterates through each list in the input and extends a result list with all elements from each one. It processes lists in order, so `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`.
|
||||||
|
|
||||||
|
The block includes validation to ensure each item is actually a list. If a non-list value (like a string or number) is encountered, the block outputs an error message instead of proceeding. None values are skipped automatically.
|
||||||
|
<!-- END MANUAL -->
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
|
||||||
|
| Input | Description | Type | Required |
|
||||||
|
|-------|-------------|------|----------|
|
||||||
|
| lists | A list of lists to concatenate together. All lists will be combined in order into a single list. | List[List[Any]] | Yes |
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
|
||||||
|
| Output | Description | Type |
|
||||||
|
|--------|-------------|------|
|
||||||
|
| error | Error message if concatenation failed due to invalid input types. | str |
|
||||||
|
| concatenated_list | The concatenated list containing all elements from all input lists in order. | List[Any] |
|
||||||
|
|
||||||
|
### Possible use case
|
||||||
|
<!-- MANUAL: use_case -->
|
||||||
|
**Paginated API Merging**: Combine results from multiple API pages into a single list for batch processing or display.
|
||||||
|
|
||||||
|
**Parallel Task Aggregation**: Merge outputs from parallel workflow branches that each produce a list of results.
|
||||||
|
|
||||||
|
**Multi-Source Data Collection**: Combine data collected from different sources (like multiple RSS feeds or API endpoints) into one unified list.
|
||||||
|
<!-- END MANUAL -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Dictionary Is Empty
|
## Dictionary Is Empty
|
||||||
|
|
||||||
### What it is
|
### What it is
|
||||||
|
|||||||
Reference in New Issue
Block a user