mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-13 00:58:16 -05:00
Compare commits
2 Commits
fix/sql-in
...
seer/fix-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a4e4defa | ||
|
|
f4ba02f2f1 |
@@ -104,6 +104,8 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
|
||||
CLAUDE_4_5_SONNET = "claude-sonnet-4-5-20250929"
|
||||
CLAUDE_4_5_HAIKU = "claude-haiku-4-5-20251001"
|
||||
CLAUDE_3_7_SONNET = "claude-3-7-sonnet-20250219"
|
||||
CLAUDE_3_5_SONNET = "claude-3-5-sonnet-latest"
|
||||
CLAUDE_3_5_HAIKU = "claude-3-5-haiku-latest"
|
||||
CLAUDE_3_HAIKU = "claude-3-haiku-20240307"
|
||||
# AI/ML API models
|
||||
AIML_API_QWEN2_5_72B = "Qwen/Qwen2.5-72B-Instruct-Turbo"
|
||||
@@ -222,6 +224,12 @@ MODEL_METADATA = {
|
||||
LlmModel.CLAUDE_3_7_SONNET: ModelMetadata(
|
||||
"anthropic", 200000, 64000
|
||||
), # claude-3-7-sonnet-20250219
|
||||
LlmModel.CLAUDE_3_5_SONNET: ModelMetadata(
|
||||
"anthropic", 200000, 8192
|
||||
), # claude-3-5-sonnet-20241022
|
||||
LlmModel.CLAUDE_3_5_HAIKU: ModelMetadata(
|
||||
"anthropic", 200000, 8192
|
||||
), # claude-3-5-haiku-20241022
|
||||
LlmModel.CLAUDE_3_HAIKU: ModelMetadata(
|
||||
"anthropic", 200000, 4096
|
||||
), # claude-3-haiku-20240307
|
||||
@@ -1554,9 +1562,7 @@ class AIConversationBlock(AIBlockBase):
|
||||
("prompt", list),
|
||||
],
|
||||
test_mock={
|
||||
"llm_call": lambda *args, **kwargs: dict(
|
||||
response="The 2020 World Series was played at Globe Life Field in Arlington, Texas."
|
||||
)
|
||||
"llm_call": lambda *args, **kwargs: "The 2020 World Series was played at Globe Life Field in Arlington, Texas."
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1585,7 +1591,7 @@ class AIConversationBlock(AIBlockBase):
|
||||
),
|
||||
credentials=credentials,
|
||||
)
|
||||
yield "response", response["response"]
|
||||
yield "response", response
|
||||
yield "prompt", self.prompt
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import signal
|
||||
import threading
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
|
||||
@@ -27,13 +26,6 @@ from backend.sdk import (
|
||||
SchemaField,
|
||||
)
|
||||
|
||||
# Suppress false positive cleanup warning of litellm (a dependency of stagehand)
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message="coroutine 'close_litellm_async_clients' was never awaited",
|
||||
category=RuntimeWarning,
|
||||
)
|
||||
|
||||
# Store the original method
|
||||
original_register_signal_handlers = stagehand.main.Stagehand._register_signal_handlers
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ class TestLLMStatsTracking:
|
||||
assert block.execution_stats.llm_call_count == 1
|
||||
|
||||
# Check output
|
||||
assert outputs["response"] == "AI response to conversation"
|
||||
assert outputs["response"] == {"response": "AI response to conversation"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ai_list_generator_with_retries(self):
|
||||
|
||||
@@ -45,6 +45,9 @@ class MainApp(AppProcess):
|
||||
|
||||
app.main(silent=True)
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
|
||||
@click.group()
|
||||
def main():
|
||||
|
||||
@@ -76,6 +76,8 @@ MODEL_COST: dict[LlmModel, int] = {
|
||||
LlmModel.CLAUDE_4_5_HAIKU: 4,
|
||||
LlmModel.CLAUDE_4_5_SONNET: 9,
|
||||
LlmModel.CLAUDE_3_7_SONNET: 5,
|
||||
LlmModel.CLAUDE_3_5_SONNET: 4,
|
||||
LlmModel.CLAUDE_3_5_HAIKU: 1, # $0.80 / $4.00
|
||||
LlmModel.CLAUDE_3_HAIKU: 1,
|
||||
LlmModel.AIML_API_QWEN2_5_72B: 1,
|
||||
LlmModel.AIML_API_LLAMA3_1_70B: 1,
|
||||
|
||||
@@ -264,7 +264,16 @@ class BaseGraph(BaseDbModel):
|
||||
schema_fields: list[AgentInputBlock.Input | AgentOutputBlock.Input] = []
|
||||
for type_class, input_default in props:
|
||||
try:
|
||||
schema_fields.append(type_class.model_construct(**input_default))
|
||||
constructed_obj = type_class.model_construct(**input_default)
|
||||
# Validate that the constructed object has required 'name' attribute
|
||||
# model_construct() bypasses validation, so we need to check manually
|
||||
if not hasattr(constructed_obj, "name") or constructed_obj.name is None:
|
||||
logger.warning(
|
||||
f"Skipping invalid {type_class.__name__} node: missing required 'name' field. "
|
||||
f"Input data: {input_default}"
|
||||
)
|
||||
continue
|
||||
schema_fields.append(constructed_obj)
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid {type_class}: {input_default}, {e}")
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Callable, Concatenate, ParamSpec, TypeVar, cast
|
||||
from typing import Callable, Concatenate, ParamSpec, TypeVar, cast
|
||||
|
||||
from backend.data import db
|
||||
from backend.data.credit import UsageTransactionMetadata, get_user_credit_model
|
||||
@@ -58,9 +57,6 @@ from backend.util.service import (
|
||||
)
|
||||
from backend.util.settings import Config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import FastAPI
|
||||
|
||||
config = Config()
|
||||
logger = logging.getLogger(__name__)
|
||||
P = ParamSpec("P")
|
||||
@@ -80,17 +76,15 @@ async def _get_credits(user_id: str) -> int:
|
||||
|
||||
|
||||
class DatabaseManager(AppService):
|
||||
@asynccontextmanager
|
||||
async def lifespan(self, app: "FastAPI"):
|
||||
async with super().lifespan(app):
|
||||
logger.info(f"[{self.service_name}] ⏳ Connecting to Database...")
|
||||
await db.connect()
|
||||
def run_service(self) -> None:
|
||||
logger.info(f"[{self.service_name}] ⏳ Connecting to Database...")
|
||||
self.run_and_wait(db.connect())
|
||||
super().run_service()
|
||||
|
||||
logger.info(f"[{self.service_name}] ✅ Ready")
|
||||
yield
|
||||
|
||||
logger.info(f"[{self.service_name}] ⏳ Disconnecting Database...")
|
||||
await db.disconnect()
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
logger.info(f"[{self.service_name}] ⏳ Disconnecting Database...")
|
||||
self.run_and_wait(db.disconnect())
|
||||
|
||||
async def health_check(self) -> str:
|
||||
if not db.is_connected():
|
||||
|
||||
@@ -1714,8 +1714,6 @@ class ExecutionManager(AppProcess):
|
||||
|
||||
logger.info(f"{prefix} ✅ Finished GraphExec cleanup")
|
||||
|
||||
super().cleanup()
|
||||
|
||||
|
||||
# ------- UTILITIES ------- #
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ class Scheduler(AppService):
|
||||
raise UnhealthyServiceError("Scheduler is still initializing")
|
||||
|
||||
# Check if we're in the middle of cleanup
|
||||
if self._shutting_down:
|
||||
if self.cleaned_up:
|
||||
return await super().health_check()
|
||||
|
||||
# Normal operation - check if scheduler is running
|
||||
@@ -375,6 +375,7 @@ class Scheduler(AppService):
|
||||
super().run_service()
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
if self.scheduler:
|
||||
logger.info("⏳ Shutting down scheduler...")
|
||||
self.scheduler.shutdown(wait=True)
|
||||
@@ -389,7 +390,7 @@ class Scheduler(AppService):
|
||||
logger.info("⏳ Waiting for event loop thread to finish...")
|
||||
_event_loop_thread.join(timeout=SCHEDULER_OPERATION_TIMEOUT_SECONDS)
|
||||
|
||||
super().cleanup()
|
||||
logger.info("Scheduler cleanup complete.")
|
||||
|
||||
@expose
|
||||
def add_graph_execution_schedule(
|
||||
|
||||
@@ -1017,14 +1017,10 @@ class NotificationManager(AppService):
|
||||
logger.exception(f"Fatal error in consumer for {queue_name}: {e}")
|
||||
raise
|
||||
|
||||
def run_service(self):
|
||||
# Queue the main _run_service task
|
||||
asyncio.run_coroutine_threadsafe(self._run_service(), self.shared_event_loop)
|
||||
|
||||
# Start the main event loop
|
||||
super().run_service()
|
||||
|
||||
@continuous_retry()
|
||||
def run_service(self):
|
||||
self.run_and_wait(self._run_service())
|
||||
|
||||
async def _run_service(self):
|
||||
logger.info(f"[{self.service_name}] ⏳ Configuring RabbitMQ...")
|
||||
self.rabbitmq_service = rabbitmq.AsyncRabbitMQ(self.rabbitmq_config)
|
||||
@@ -1090,10 +1086,9 @@ class NotificationManager(AppService):
|
||||
def cleanup(self):
|
||||
"""Cleanup service resources"""
|
||||
self.running = False
|
||||
logger.info("⏳ Disconnecting RabbitMQ...")
|
||||
self.run_and_wait(self.rabbitmq_service.disconnect())
|
||||
|
||||
super().cleanup()
|
||||
logger.info(f"[{self.service_name}] ⏳ Disconnecting RabbitMQ...")
|
||||
self.run_and_wait(self.rabbitmq_service.disconnect())
|
||||
|
||||
|
||||
class NotificationManagerClient(AppServiceClient):
|
||||
|
||||
@@ -321,6 +321,10 @@ class AgentServer(backend.util.service.AppProcess):
|
||||
|
||||
uvicorn.run(**uvicorn_config)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
logger.info(f"[{self.service_name}] ⏳ Shutting down Agent Server...")
|
||||
|
||||
@staticmethod
|
||||
async def test_execute_graph(
|
||||
graph_id: str,
|
||||
|
||||
@@ -73,15 +73,6 @@ async def get_store_agents(
|
||||
f"Getting store agents. featured={featured}, creators={creators}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}"
|
||||
)
|
||||
|
||||
sanitized_creators = []
|
||||
if creators:
|
||||
for c in creators:
|
||||
sanitized_creators.append(sanitize_query(c))
|
||||
|
||||
sanitized_category = None
|
||||
if category:
|
||||
sanitized_category = sanitize_query(category)
|
||||
|
||||
try:
|
||||
# If search_query is provided, use full-text search
|
||||
if search_query:
|
||||
@@ -100,50 +91,33 @@ async def get_store_agents(
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# Whitelist allowed order_by columns
|
||||
ALLOWED_ORDER_BY = {
|
||||
"rating": "rating DESC, rank DESC",
|
||||
"runs": "runs DESC, rank DESC",
|
||||
"name": "agent_name ASC, rank DESC",
|
||||
"updated_at": "updated_at DESC, rank DESC",
|
||||
}
|
||||
|
||||
# Validate and get order clause
|
||||
if sorted_by and sorted_by in ALLOWED_ORDER_BY:
|
||||
order_by_clause = ALLOWED_ORDER_BY[sorted_by]
|
||||
else:
|
||||
order_by_clause = "updated_at DESC, rank DESC",
|
||||
|
||||
# Build WHERE conditions and parameters list
|
||||
where_parts: list[str] = []
|
||||
params: list[typing.Any] = [search_term] # $1 - search term
|
||||
param_index = 2 # Start at $2 for next parameter
|
||||
|
||||
# Always filter for available agents
|
||||
where_parts.append("is_available = true")
|
||||
# Build filter conditions
|
||||
filter_conditions = []
|
||||
filter_conditions.append("is_available = true")
|
||||
|
||||
if featured:
|
||||
where_parts.append("featured = true")
|
||||
filter_conditions.append("featured = true")
|
||||
if creators:
|
||||
creator_list = "','".join(creators)
|
||||
filter_conditions.append(f"creator_username IN ('{creator_list}')")
|
||||
if category:
|
||||
filter_conditions.append(f"'{category}' = ANY(categories)")
|
||||
|
||||
if creators and sanitized_creators:
|
||||
# Use ANY with array parameter
|
||||
where_parts.append(f"creator_username = ANY(${param_index})")
|
||||
params.append(sanitized_creators)
|
||||
param_index += 1
|
||||
where_filter = (
|
||||
" AND ".join(filter_conditions) if filter_conditions else "1=1"
|
||||
)
|
||||
|
||||
if category and sanitized_category:
|
||||
where_parts.append(f"${param_index} = ANY(categories)")
|
||||
params.append(sanitized_category)
|
||||
param_index += 1
|
||||
# Build ORDER BY clause
|
||||
if sorted_by == "rating":
|
||||
order_by_clause = "rating DESC, rank DESC"
|
||||
elif sorted_by == "runs":
|
||||
order_by_clause = "runs DESC, rank DESC"
|
||||
elif sorted_by == "name":
|
||||
order_by_clause = "agent_name ASC, rank DESC"
|
||||
else:
|
||||
order_by_clause = "rank DESC, updated_at DESC"
|
||||
|
||||
sql_where_clause: str = " AND ".join(where_parts) if where_parts else "1=1"
|
||||
|
||||
# Add pagination params
|
||||
params.extend([page_size, offset])
|
||||
limit_param = f"${param_index}"
|
||||
offset_param = f"${param_index + 1}"
|
||||
|
||||
# Execute full-text search query with parameterized values
|
||||
# Execute full-text search query
|
||||
sql_query = f"""
|
||||
SELECT
|
||||
slug,
|
||||
@@ -161,31 +135,29 @@ async def get_store_agents(
|
||||
updated_at,
|
||||
ts_rank_cd(search, query) AS rank
|
||||
FROM "StoreAgent",
|
||||
plainto_tsquery('english', $1) AS query
|
||||
WHERE {sql_where_clause}
|
||||
plainto_tsquery('english', '{search_term}') AS query
|
||||
WHERE {where_filter}
|
||||
AND search @@ query
|
||||
ORDER BY {order_by_clause}
|
||||
LIMIT {limit_param} OFFSET {offset_param}
|
||||
ORDER BY rank DESC, {order_by_clause}
|
||||
LIMIT {page_size} OFFSET {offset}
|
||||
"""
|
||||
|
||||
# Count query for pagination - only uses search term parameter
|
||||
# Count query for pagination
|
||||
count_query = f"""
|
||||
SELECT COUNT(*) as count
|
||||
FROM "StoreAgent",
|
||||
plainto_tsquery('english', $1) AS query
|
||||
WHERE {sql_where_clause}
|
||||
plainto_tsquery('english', '{search_term}') AS query
|
||||
WHERE {where_filter}
|
||||
AND search @@ query
|
||||
"""
|
||||
|
||||
# Execute both queries with parameters
|
||||
# Execute both queries
|
||||
agents = await prisma.client.get_client().query_raw(
|
||||
typing.cast(typing.LiteralString, sql_query), *params
|
||||
query=typing.cast(typing.LiteralString, sql_query)
|
||||
)
|
||||
|
||||
# For count, use params without pagination (last 2 params)
|
||||
count_params = params[:-2]
|
||||
count_result = await prisma.client.get_client().query_raw(
|
||||
typing.cast(typing.LiteralString, count_query), *count_params
|
||||
query=typing.cast(typing.LiteralString, count_query)
|
||||
)
|
||||
|
||||
total = count_result[0]["count"] if count_result else 0
|
||||
@@ -219,9 +191,9 @@ async def get_store_agents(
|
||||
if featured:
|
||||
where_clause["featured"] = featured
|
||||
if creators:
|
||||
where_clause["creator_username"] = {"in": sanitized_creators}
|
||||
if sanitized_category:
|
||||
where_clause["categories"] = {"has": sanitized_category}
|
||||
where_clause["creator_username"] = {"in": creators}
|
||||
if category:
|
||||
where_clause["categories"] = {"has": category}
|
||||
|
||||
order_by = []
|
||||
if sorted_by == "rating":
|
||||
|
||||
@@ -20,7 +20,7 @@ async def setup_prisma():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_store_agents(mocker):
|
||||
# Mock data
|
||||
mock_agents = [
|
||||
@@ -64,7 +64,7 @@ async def test_get_store_agents(mocker):
|
||||
mock_store_agent.return_value.count.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_store_agent_details(mocker):
|
||||
# Mock data
|
||||
mock_agent = prisma.models.StoreAgent(
|
||||
@@ -173,7 +173,7 @@ async def test_get_store_agent_details(mocker):
|
||||
mock_store_listing_db.return_value.find_first.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_store_creator_details(mocker):
|
||||
# Mock data
|
||||
mock_creator_data = prisma.models.Creator(
|
||||
@@ -210,7 +210,7 @@ async def test_get_store_creator_details(mocker):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_store_submission(mocker):
|
||||
# Mock data
|
||||
mock_agent = prisma.models.AgentGraph(
|
||||
@@ -282,7 +282,7 @@ async def test_create_store_submission(mocker):
|
||||
mock_store_listing.return_value.create.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_profile(mocker):
|
||||
# Mock data
|
||||
mock_profile = prisma.models.Profile(
|
||||
@@ -327,7 +327,7 @@ async def test_update_profile(mocker):
|
||||
mock_profile_db.return_value.update.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_profile(mocker):
|
||||
# Mock data
|
||||
mock_profile = prisma.models.Profile(
|
||||
@@ -359,63 +359,3 @@ async def test_get_user_profile(mocker):
|
||||
assert result.description == "Test description"
|
||||
assert result.links == ["link1", "link2"]
|
||||
assert result.avatar_url == "avatar.jpg"
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_store_agents_with_search_parameterized(mocker):
|
||||
"""Test that search query uses parameterized SQL - validates the fix works"""
|
||||
|
||||
# Call function with search query containing potential SQL injection
|
||||
malicious_search = "test'; DROP TABLE StoreAgent; --"
|
||||
result = await db.get_store_agents(search_query=malicious_search)
|
||||
|
||||
# Verify query executed safely
|
||||
assert isinstance(result.agents, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_store_agents_with_search_and_filters_parameterized():
|
||||
"""Test parameterized SQL with multiple filters"""
|
||||
|
||||
# Call with multiple filters including potential injection attempts
|
||||
result = await db.get_store_agents(
|
||||
search_query="test",
|
||||
creators=["creator1'; DROP TABLE Users; --", "creator2"],
|
||||
category="AI'; DELETE FROM StoreAgent; --",
|
||||
featured=True,
|
||||
sorted_by="rating",
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
# Verify the query executed without error
|
||||
assert isinstance(result.agents, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_store_agents_search_with_invalid_sort_by():
|
||||
"""Test that invalid sorted_by value doesn't cause SQL injection""" # Try to inject SQL via sorted_by parameter
|
||||
malicious_sort = "rating; DROP TABLE Users; --"
|
||||
result = await db.get_store_agents(
|
||||
search_query="test",
|
||||
sorted_by=malicious_sort,
|
||||
)
|
||||
|
||||
# Verify the query executed without error
|
||||
# Invalid sort_by should fall back to default, not cause SQL injection
|
||||
assert isinstance(result.agents, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_store_agents_search_category_array_injection():
|
||||
"""Test that category parameter is safely passed as a parameter"""
|
||||
# Try SQL injection via category
|
||||
malicious_category = "AI'; DROP TABLE StoreAgent; --"
|
||||
result = await db.get_store_agents(
|
||||
search_query="test",
|
||||
category=malicious_category,
|
||||
)
|
||||
|
||||
# Verify the query executed without error
|
||||
# Category should be parameterized, preventing SQL injection
|
||||
assert isinstance(result.agents, list)
|
||||
|
||||
@@ -329,3 +329,7 @@ class WebsocketServer(AppProcess):
|
||||
port=Config().websocket_server_port,
|
||||
log_config=None,
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
logger.info(f"[{self.service_name}] ⏳ Shutting down WebSocket Server...")
|
||||
|
||||
@@ -19,8 +19,7 @@ class AppProcess(ABC):
|
||||
"""
|
||||
|
||||
process: Optional[Process] = None
|
||||
_shutting_down: bool = False
|
||||
_cleaned_up: bool = False
|
||||
cleaned_up = False
|
||||
|
||||
if "forkserver" in get_all_start_methods():
|
||||
set_start_method("forkserver", force=True)
|
||||
@@ -44,6 +43,7 @@ class AppProcess(ABC):
|
||||
def service_name(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
@abstractmethod
|
||||
def cleanup(self):
|
||||
"""
|
||||
Implement this method on a subclass to do post-execution cleanup,
|
||||
@@ -65,8 +65,7 @@ class AppProcess(ABC):
|
||||
self.run()
|
||||
except BaseException as e:
|
||||
logger.warning(
|
||||
f"[{self.service_name}] 🛑 Terminating because of {type(e).__name__}: {e}", # noqa
|
||||
exc_info=e if not isinstance(e, SystemExit) else None,
|
||||
f"[{self.service_name}] Termination request: {type(e).__name__}; {e} executing cleanup."
|
||||
)
|
||||
# Send error to Sentry before cleanup
|
||||
if not isinstance(e, (KeyboardInterrupt, SystemExit)):
|
||||
@@ -77,12 +76,8 @@ class AppProcess(ABC):
|
||||
except Exception:
|
||||
pass # Silently ignore if Sentry isn't available
|
||||
finally:
|
||||
if not self._cleaned_up:
|
||||
self._cleaned_up = True
|
||||
logger.info(f"[{self.service_name}] 🧹 Running cleanup")
|
||||
self.cleanup()
|
||||
logger.info(f"[{self.service_name}] ✅ Cleanup done")
|
||||
logger.info(f"[{self.service_name}] 🛑 Terminated")
|
||||
self.cleanup()
|
||||
logger.info(f"[{self.service_name}] Terminated.")
|
||||
|
||||
@staticmethod
|
||||
def llprint(message: str):
|
||||
@@ -93,8 +88,8 @@ class AppProcess(ABC):
|
||||
os.write(sys.stdout.fileno(), (message + "\n").encode())
|
||||
|
||||
def _self_terminate(self, signum: int, frame):
|
||||
if not self._shutting_down:
|
||||
self._shutting_down = True
|
||||
if not self.cleaned_up:
|
||||
self.cleaned_up = True
|
||||
sys.exit(0)
|
||||
else:
|
||||
self.llprint(
|
||||
|
||||
@@ -175,15 +175,10 @@ async def validate_url(
|
||||
f"for hostname {ascii_hostname} is not allowed."
|
||||
)
|
||||
|
||||
# Reconstruct the netloc with IDNA-encoded hostname and preserve port
|
||||
netloc = ascii_hostname
|
||||
if parsed.port:
|
||||
netloc = f"{ascii_hostname}:{parsed.port}"
|
||||
|
||||
return (
|
||||
URL(
|
||||
parsed.scheme,
|
||||
netloc,
|
||||
ascii_hostname,
|
||||
quote(parsed.path, safe="/%:@"),
|
||||
parsed.params,
|
||||
parsed.query,
|
||||
|
||||
@@ -4,12 +4,9 @@ import concurrent.futures
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import update_wrapper
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -114,44 +111,14 @@ class BaseAppService(AppProcess, ABC):
|
||||
return target_host
|
||||
|
||||
def run_service(self) -> None:
|
||||
# HACK: run the main event loop outside the main thread to disable Uvicorn's
|
||||
# internal signal handlers, since there is no config option for this :(
|
||||
shared_asyncio_thread = threading.Thread(
|
||||
target=self._run_shared_event_loop,
|
||||
daemon=True,
|
||||
name=f"{self.service_name}-shared-event-loop",
|
||||
)
|
||||
shared_asyncio_thread.start()
|
||||
shared_asyncio_thread.join()
|
||||
|
||||
def _run_shared_event_loop(self) -> None:
|
||||
try:
|
||||
self.shared_event_loop.run_forever()
|
||||
finally:
|
||||
logger.info(f"[{self.service_name}] 🛑 Shared event loop stopped")
|
||||
self.shared_event_loop.close() # ensure held resources are released
|
||||
while True:
|
||||
time.sleep(10)
|
||||
|
||||
def run_and_wait(self, coro: Coroutine[Any, Any, T]) -> T:
|
||||
return asyncio.run_coroutine_threadsafe(coro, self.shared_event_loop).result()
|
||||
|
||||
def run(self):
|
||||
self.shared_event_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.shared_event_loop)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
**💡 Overriding `AppService.lifespan` may be a more convenient option.**
|
||||
|
||||
Implement this method on a subclass to do post-execution cleanup,
|
||||
e.g. disconnecting from a database or terminating child processes.
|
||||
|
||||
**Note:** if you override this method in a subclass, it must call
|
||||
`super().cleanup()` *at the end*!
|
||||
"""
|
||||
# Stop the shared event loop to allow resource clean-up
|
||||
self.shared_event_loop.call_soon_threadsafe(self.shared_event_loop.stop)
|
||||
|
||||
super().cleanup()
|
||||
self.shared_event_loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
class RemoteCallError(BaseModel):
|
||||
@@ -212,7 +179,6 @@ EXCEPTION_MAPPING = {
|
||||
|
||||
class AppService(BaseAppService, ABC):
|
||||
fastapi_app: FastAPI
|
||||
http_server: uvicorn.Server | None = None
|
||||
log_level: str = "info"
|
||||
|
||||
def set_log_level(self, log_level: str):
|
||||
@@ -224,10 +190,11 @@ class AppService(BaseAppService, ABC):
|
||||
def _handle_internal_http_error(status_code: int = 500, log_error: bool = True):
|
||||
def handler(request: Request, exc: Exception):
|
||||
if log_error:
|
||||
logger.error(
|
||||
f"{request.method} {request.url.path} failed: {exc}",
|
||||
exc_info=exc if status_code == 500 else None,
|
||||
)
|
||||
if status_code == 500:
|
||||
log = logger.exception
|
||||
else:
|
||||
log = logger.error
|
||||
log(f"{request.method} {request.url.path} failed: {exc}")
|
||||
return responses.JSONResponse(
|
||||
status_code=status_code,
|
||||
content=RemoteCallError(
|
||||
@@ -289,13 +256,13 @@ class AppService(BaseAppService, ABC):
|
||||
|
||||
return sync_endpoint
|
||||
|
||||
@conn_retry("FastAPI server", "Running FastAPI server")
|
||||
@conn_retry("FastAPI server", "Starting FastAPI server")
|
||||
def __start_fastapi(self):
|
||||
logger.info(
|
||||
f"[{self.service_name}] Starting RPC server at http://{api_host}:{self.get_port()}"
|
||||
)
|
||||
|
||||
self.http_server = uvicorn.Server(
|
||||
server = uvicorn.Server(
|
||||
uvicorn.Config(
|
||||
self.fastapi_app,
|
||||
host=api_host,
|
||||
@@ -304,76 +271,18 @@ class AppService(BaseAppService, ABC):
|
||||
log_level=self.log_level,
|
||||
)
|
||||
)
|
||||
self.run_and_wait(self.http_server.serve())
|
||||
|
||||
# Perform clean-up when the server exits
|
||||
if not self._cleaned_up:
|
||||
self._cleaned_up = True
|
||||
logger.info(f"[{self.service_name}] 🧹 Running cleanup")
|
||||
self.cleanup()
|
||||
logger.info(f"[{self.service_name}] ✅ Cleanup done")
|
||||
|
||||
def _self_terminate(self, signum: int, frame):
|
||||
"""Pass SIGTERM to Uvicorn so it can shut down gracefully"""
|
||||
signame = signal.Signals(signum).name
|
||||
if not self._shutting_down:
|
||||
self._shutting_down = True
|
||||
if self.http_server:
|
||||
logger.info(
|
||||
f"[{self.service_name}] 🛑 Received {signame} ({signum}) - "
|
||||
"Entering RPC server graceful shutdown"
|
||||
)
|
||||
self.http_server.handle_exit(signum, frame) # stop accepting requests
|
||||
|
||||
# NOTE: Actually stopping the process is triggered by:
|
||||
# 1. The call to self.cleanup() at the end of __start_fastapi() 👆🏼
|
||||
# 2. BaseAppService.cleanup() stopping the shared event loop
|
||||
else:
|
||||
logger.warning(
|
||||
f"[{self.service_name}] {signame} received before HTTP server init."
|
||||
" Terminating..."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
# Expedite shutdown on second SIGTERM
|
||||
logger.info(
|
||||
f"[{self.service_name}] 🛑🛑 Received {signame} ({signum}), "
|
||||
"but shutdown is already underway. Terminating..."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(self, app: FastAPI):
|
||||
"""
|
||||
The FastAPI/Uvicorn server's lifespan manager, used for setup and shutdown.
|
||||
|
||||
You can extend and use this in a subclass like:
|
||||
```
|
||||
@asynccontextmanager
|
||||
async def lifespan(self, app: FastAPI):
|
||||
async with super().lifespan(app):
|
||||
await db.connect()
|
||||
yield
|
||||
await db.disconnect()
|
||||
```
|
||||
"""
|
||||
# Startup - this runs before Uvicorn starts accepting connections
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown - this runs when FastAPI/Uvicorn shuts down
|
||||
logger.info(f"[{self.service_name}] ✅ FastAPI has finished")
|
||||
self.shared_event_loop.run_until_complete(server.serve())
|
||||
|
||||
async def health_check(self) -> str:
|
||||
"""A method to check the health of the process."""
|
||||
"""
|
||||
A method to check the health of the process.
|
||||
"""
|
||||
return "OK"
|
||||
|
||||
def run(self):
|
||||
sentry_init()
|
||||
super().run()
|
||||
|
||||
self.fastapi_app = FastAPI(lifespan=self.lifespan)
|
||||
self.fastapi_app = FastAPI()
|
||||
|
||||
# Add Prometheus instrumentation to all services
|
||||
try:
|
||||
@@ -416,11 +325,7 @@ class AppService(BaseAppService, ABC):
|
||||
)
|
||||
|
||||
# Start the FastAPI server in a separate thread.
|
||||
api_thread = threading.Thread(
|
||||
target=self.__start_fastapi,
|
||||
daemon=True,
|
||||
name=f"{self.service_name}-http-server",
|
||||
)
|
||||
api_thread = threading.Thread(target=self.__start_fastapi, daemon=True)
|
||||
api_thread.start()
|
||||
|
||||
# Run the main service loop (blocking).
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import time
|
||||
from functools import cached_property
|
||||
from unittest.mock import Mock
|
||||
@@ -20,11 +18,20 @@ from backend.util.service import (
|
||||
TEST_SERVICE_PORT = 8765
|
||||
|
||||
|
||||
def wait_for_service_ready(service_client_type, timeout_seconds=30):
|
||||
"""Helper method to wait for a service to be ready using health check with retry."""
|
||||
client = get_service_client(service_client_type, request_retry=True)
|
||||
client.health_check() # This will retry until service is ready
|
||||
|
||||
|
||||
class ServiceTest(AppService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.fail_count = 0
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_port(cls) -> int:
|
||||
return TEST_SERVICE_PORT
|
||||
@@ -34,17 +41,10 @@ class ServiceTest(AppService):
|
||||
result = super().__enter__()
|
||||
|
||||
# Wait for the service to be ready
|
||||
self.wait_until_ready()
|
||||
wait_for_service_ready(ServiceTestClient)
|
||||
|
||||
return result
|
||||
|
||||
def wait_until_ready(self, timeout_seconds: int = 5):
|
||||
"""Helper method to wait for a service to be ready using health check with retry."""
|
||||
client = get_service_client(
|
||||
ServiceTestClient, call_timeout=timeout_seconds, request_retry=True
|
||||
)
|
||||
client.health_check() # This will retry until service is ready\
|
||||
|
||||
@expose
|
||||
def add(self, a: int, b: int) -> int:
|
||||
return a + b
|
||||
@@ -490,167 +490,3 @@ class TestHTTPErrorRetryBehavior:
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == status_code
|
||||
|
||||
|
||||
class TestGracefulShutdownService(AppService):
|
||||
"""Test service with slow endpoints for testing graceful shutdown"""
|
||||
|
||||
@classmethod
|
||||
def get_port(cls) -> int:
|
||||
return 18999 # Use a specific test port
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.request_log = []
|
||||
self.cleanup_called = False
|
||||
self.cleanup_completed = False
|
||||
|
||||
@expose
|
||||
async def slow_endpoint(self, duration: int = 5) -> dict:
|
||||
"""Endpoint that takes time to complete"""
|
||||
start_time = time.time()
|
||||
self.request_log.append(f"slow_endpoint started at {start_time}")
|
||||
|
||||
await asyncio.sleep(duration)
|
||||
|
||||
end_time = time.time()
|
||||
result = {
|
||||
"message": "completed",
|
||||
"duration": end_time - start_time,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
}
|
||||
self.request_log.append(f"slow_endpoint completed at {end_time}")
|
||||
return result
|
||||
|
||||
@expose
|
||||
def fast_endpoint(self) -> dict:
|
||||
"""Fast endpoint for testing rejection during shutdown"""
|
||||
timestamp = time.time()
|
||||
self.request_log.append(f"fast_endpoint called at {timestamp}")
|
||||
return {"message": "fast", "timestamp": timestamp}
|
||||
|
||||
def cleanup(self):
|
||||
"""Override cleanup to track when it's called"""
|
||||
self.cleanup_called = True
|
||||
self.request_log.append(f"cleanup started at {time.time()}")
|
||||
|
||||
# Call parent cleanup
|
||||
super().cleanup()
|
||||
|
||||
self.cleanup_completed = True
|
||||
self.request_log.append(f"cleanup completed at {time.time()}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def test_service():
|
||||
"""Run the test service in a separate process"""
|
||||
|
||||
service = TestGracefulShutdownService()
|
||||
service.start(background=True)
|
||||
|
||||
base_url = f"http://localhost:{service.get_port()}"
|
||||
|
||||
await wait_until_service_ready(base_url)
|
||||
yield service, base_url
|
||||
|
||||
service.stop()
|
||||
|
||||
|
||||
async def wait_until_service_ready(base_url: str, timeout: float = 10):
|
||||
start_time = time.time()
|
||||
while time.time() - start_time <= timeout:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
with contextlib.suppress(httpx.ConnectError):
|
||||
response = await client.get(f"{base_url}/health_check", timeout=5)
|
||||
|
||||
if response.status_code == 200 and response.json() == "OK":
|
||||
return
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
raise RuntimeError(f"Service at {base_url} not available after {timeout} seconds")
|
||||
|
||||
|
||||
async def send_slow_request(base_url: str) -> dict:
|
||||
"""Send a slow request and return the result"""
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
response = await client.post(f"{base_url}/slow_endpoint", json={"duration": 5})
|
||||
assert response.status_code == 200
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_graceful_shutdown(test_service):
|
||||
"""Test that AppService handles graceful shutdown correctly"""
|
||||
service, test_service_url = test_service
|
||||
|
||||
# Start a slow request that should complete even after shutdown
|
||||
slow_task = asyncio.create_task(send_slow_request(test_service_url))
|
||||
|
||||
# Give the slow request time to start
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Send SIGTERM to the service process
|
||||
shutdown_start_time = time.time()
|
||||
service.process.terminate() # This sends SIGTERM
|
||||
|
||||
# Wait a moment for shutdown to start
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Try to send a new request - should be rejected or connection refused
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.post(f"{test_service_url}/fast_endpoint", json={})
|
||||
# Should get 503 Service Unavailable during shutdown
|
||||
assert response.status_code == 503
|
||||
assert "shutting down" in response.json()["detail"].lower()
|
||||
except httpx.ConnectError:
|
||||
# Connection refused is also acceptable - server stopped accepting
|
||||
pass
|
||||
|
||||
# The slow request should still complete successfully
|
||||
slow_result = await slow_task
|
||||
assert slow_result["message"] == "completed"
|
||||
assert 4.9 < slow_result["duration"] < 5.5 # Should have taken ~5 seconds
|
||||
|
||||
# Wait for the service to fully shut down
|
||||
service.process.join(timeout=15)
|
||||
shutdown_end_time = time.time()
|
||||
|
||||
# Verify the service actually terminated
|
||||
assert not service.process.is_alive()
|
||||
|
||||
# Verify shutdown took reasonable time (slow request - 1s + cleanup)
|
||||
shutdown_duration = shutdown_end_time - shutdown_start_time
|
||||
assert 4 <= shutdown_duration <= 6 # ~5s request - 1s + buffer
|
||||
|
||||
print(f"Shutdown took {shutdown_duration:.2f} seconds")
|
||||
print(f"Slow request completed in: {slow_result['duration']:.2f} seconds")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_during_shutdown(test_service):
|
||||
"""Test that health checks behave correctly during shutdown"""
|
||||
service, test_service_url = test_service
|
||||
|
||||
# Health check should pass initially
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.get(f"{test_service_url}/health_check")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Send SIGTERM
|
||||
service.process.terminate()
|
||||
|
||||
# Wait for shutdown to begin
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Health check should now fail or connection should be refused
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.get(f"{test_service_url}/health_check")
|
||||
# Could either get 503, 500 (unhealthy), or connection error
|
||||
assert response.status_code in [500, 503]
|
||||
except (httpx.ConnectError, httpx.ConnectTimeout):
|
||||
# Connection refused/timeout is also acceptable
|
||||
pass
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- Migrate Claude 3.5 models to Claude 4.5 models
|
||||
-- This updates all AgentNode blocks that use deprecated Claude 3.5 models to the new 4.5 models
|
||||
-- See: https://docs.anthropic.com/en/docs/about-claude/models/legacy-model-guide
|
||||
|
||||
-- Update Claude 3.5 Sonnet to Claude 4.5 Sonnet
|
||||
UPDATE "AgentNode"
|
||||
SET "constantInput" = JSONB_SET(
|
||||
"constantInput"::jsonb,
|
||||
'{model}',
|
||||
'"claude-sonnet-4-5-20250929"'::jsonb
|
||||
)
|
||||
WHERE "constantInput"::jsonb->>'model' = 'claude-3-5-sonnet-latest';
|
||||
|
||||
-- Update Claude 3.5 Haiku to Claude 4.5 Haiku
|
||||
UPDATE "AgentNode"
|
||||
SET "constantInput" = JSONB_SET(
|
||||
"constantInput"::jsonb,
|
||||
'{model}',
|
||||
'"claude-haiku-4-5-20251001"'::jsonb
|
||||
)
|
||||
WHERE "constantInput"::jsonb->>'model' = 'claude-3-5-haiku-latest';
|
||||
@@ -55,7 +55,7 @@
|
||||
"@sentry/nextjs": "10.15.0",
|
||||
"@supabase/ssr": "0.6.1",
|
||||
"@supabase/supabase-js": "2.55.0",
|
||||
"@tanstack/react-query": "5.87.1",
|
||||
"@tanstack/react-query": "5.85.3",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@types/jaro-winkler": "0.2.4",
|
||||
"@vercel/analytics": "1.5.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"shepherd.js": "14.5.1",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwind-scrollbar": "3.1.0",
|
||||
"tailwind-scrollbar": "4.0.2",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"uuid": "11.1.0",
|
||||
"vaul": "1.1.2",
|
||||
|
||||
144
autogpt_platform/frontend/pnpm-lock.yaml
generated
144
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -99,8 +99,8 @@ importers:
|
||||
specifier: 2.55.0
|
||||
version: 2.55.0
|
||||
'@tanstack/react-query':
|
||||
specifier: 5.87.1
|
||||
version: 5.87.1(react@18.3.1)
|
||||
specifier: 5.85.3
|
||||
version: 5.85.3(react@18.3.1)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -243,8 +243,8 @@ importers:
|
||||
specifier: 2.6.0
|
||||
version: 2.6.0
|
||||
tailwind-scrollbar:
|
||||
specifier: 3.1.0
|
||||
version: 3.1.0(tailwindcss@3.4.17)
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17)
|
||||
tailwindcss-animate:
|
||||
specifier: 1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17)
|
||||
@@ -287,7 +287,7 @@ importers:
|
||||
version: 5.86.0(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@tanstack/react-query-devtools':
|
||||
specifier: 5.87.3
|
||||
version: 5.87.3(@tanstack/react-query@5.87.1(react@18.3.1))(react@18.3.1)
|
||||
version: 5.87.3(@tanstack/react-query@5.85.3(react@18.3.1))(react@18.3.1)
|
||||
'@types/canvas-confetti':
|
||||
specifier: 1.9.0
|
||||
version: 1.9.0
|
||||
@@ -947,6 +947,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.3':
|
||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -981,6 +985,9 @@ packages:
|
||||
'@emnapi/core@1.5.0':
|
||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||
|
||||
'@emnapi/runtime@1.4.5':
|
||||
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
|
||||
|
||||
'@emnapi/runtime@1.5.0':
|
||||
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
|
||||
|
||||
@@ -1152,6 +1159,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0':
|
||||
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0':
|
||||
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -2843,8 +2856,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
|
||||
'@tanstack/query-core@5.87.1':
|
||||
resolution: {integrity: sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA==}
|
||||
'@tanstack/query-core@5.85.3':
|
||||
resolution: {integrity: sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ==}
|
||||
|
||||
'@tanstack/query-devtools@5.87.3':
|
||||
resolution: {integrity: sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg==}
|
||||
@@ -2855,8 +2868,8 @@ packages:
|
||||
'@tanstack/react-query': ^5.87.1
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-query@5.87.1':
|
||||
resolution: {integrity: sha512-YKauf8jfMowgAqcxj96AHs+Ux3m3bWT1oSVKamaRPXSnW2HqSznnTCEkAVqctF1e/W9R/mPcyzzINIgpOH94qg==}
|
||||
'@tanstack/react-query@5.85.3':
|
||||
resolution: {integrity: sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
@@ -3032,6 +3045,9 @@ packages:
|
||||
'@types/phoenix@1.6.6':
|
||||
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
|
||||
|
||||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
@@ -3724,6 +3740,9 @@ packages:
|
||||
camelize@1.0.1:
|
||||
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
|
||||
|
||||
caniuse-lite@1.0.30001735:
|
||||
resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
|
||||
|
||||
caniuse-lite@1.0.30001741:
|
||||
resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==}
|
||||
|
||||
@@ -4089,6 +4108,15 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -6192,6 +6220,11 @@ packages:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
||||
prism-react-renderer@2.4.1:
|
||||
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@@ -6916,11 +6949,11 @@ packages:
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
tailwind-scrollbar@3.1.0:
|
||||
resolution: {integrity: sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==}
|
||||
tailwind-scrollbar@4.0.2:
|
||||
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: 3.x
|
||||
tailwindcss: 4.x
|
||||
|
||||
tailwindcss-animate@1.0.7:
|
||||
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
|
||||
@@ -7534,7 +7567,7 @@ snapshots:
|
||||
'@babel/types': 7.28.4
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@@ -7586,7 +7619,7 @@ snapshots:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-compilation-targets': 7.27.2
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.10
|
||||
transitivePeerDependencies:
|
||||
@@ -8237,6 +8270,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/runtime@7.28.3': {}
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
@@ -8253,7 +8288,7 @@ snapshots:
|
||||
'@babel/parser': 7.28.4
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/types': 7.28.4
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8290,6 +8325,11 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.4.5':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.5.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -8386,6 +8426,11 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)':
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
@@ -8396,7 +8441,7 @@ snapshots:
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
@@ -8446,7 +8491,7 @@ snapshots:
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8547,7 +8592,7 @@ snapshots:
|
||||
|
||||
'@img/sharp-wasm32@0.34.3':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.5.0
|
||||
'@emnapi/runtime': 1.4.5
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.3':
|
||||
@@ -8996,7 +9041,7 @@ snapshots:
|
||||
ajv: 8.17.1
|
||||
chalk: 4.1.2
|
||||
compare-versions: 6.1.1
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
esbuild: 0.25.9
|
||||
esutils: 2.0.3
|
||||
fs-extra: 11.3.1
|
||||
@@ -10328,7 +10373,7 @@ snapshots:
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.9.2)(webpack@5.101.3(esbuild@0.25.9))':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
endent: 2.1.0
|
||||
find-cache-dir: 3.3.2
|
||||
flat-cache: 3.2.0
|
||||
@@ -10415,19 +10460,19 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@tanstack/query-core@5.87.1': {}
|
||||
'@tanstack/query-core@5.85.3': {}
|
||||
|
||||
'@tanstack/query-devtools@5.87.3': {}
|
||||
|
||||
'@tanstack/react-query-devtools@5.87.3(@tanstack/react-query@5.87.1(react@18.3.1))(react@18.3.1)':
|
||||
'@tanstack/react-query-devtools@5.87.3(@tanstack/react-query@5.85.3(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@tanstack/query-devtools': 5.87.3
|
||||
'@tanstack/react-query': 5.87.1(react@18.3.1)
|
||||
'@tanstack/react-query': 5.85.3(react@18.3.1)
|
||||
react: 18.3.1
|
||||
|
||||
'@tanstack/react-query@5.87.1(react@18.3.1)':
|
||||
'@tanstack/react-query@5.85.3(react@18.3.1)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.87.1
|
||||
'@tanstack/query-core': 5.85.3
|
||||
react: 18.3.1
|
||||
|
||||
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
@@ -10619,6 +10664,8 @@ snapshots:
|
||||
|
||||
'@types/phoenix@1.6.6': {}
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/react-dom@18.3.5(@types/react@18.3.17)':
|
||||
@@ -10687,7 +10734,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.43.0
|
||||
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2)
|
||||
'@typescript-eslint/visitor-keys': 8.43.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
eslint: 8.57.1
|
||||
typescript: 5.9.2
|
||||
transitivePeerDependencies:
|
||||
@@ -10697,7 +10744,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2)
|
||||
'@typescript-eslint/types': 8.43.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
typescript: 5.9.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10716,7 +10763,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.43.0
|
||||
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2)
|
||||
'@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
eslint: 8.57.1
|
||||
ts-api-utils: 2.1.0(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
@@ -10731,7 +10778,7 @@ snapshots:
|
||||
'@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2)
|
||||
'@typescript-eslint/types': 8.43.0
|
||||
'@typescript-eslint/visitor-keys': 8.43.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
@@ -11348,6 +11395,8 @@ snapshots:
|
||||
|
||||
camelize@1.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001735: {}
|
||||
|
||||
caniuse-lite@1.0.30001741: {}
|
||||
|
||||
case-sensitive-paths-webpack-plugin@2.4.0: {}
|
||||
@@ -11549,7 +11598,7 @@ snapshots:
|
||||
dependencies:
|
||||
cipher-base: 1.0.6
|
||||
inherits: 2.0.4
|
||||
ripemd160: 2.0.2
|
||||
ripemd160: 2.0.1
|
||||
sha.js: 2.4.12
|
||||
|
||||
create-hash@1.2.0:
|
||||
@@ -11563,9 +11612,9 @@ snapshots:
|
||||
create-hmac@1.1.7:
|
||||
dependencies:
|
||||
cipher-base: 1.0.6
|
||||
create-hash: 1.2.0
|
||||
create-hash: 1.1.3
|
||||
inherits: 2.0.4
|
||||
ripemd160: 2.0.2
|
||||
ripemd160: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
sha.js: 2.4.12
|
||||
|
||||
@@ -11723,6 +11772,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -12024,7 +12077,7 @@ snapshots:
|
||||
|
||||
esbuild-register@3.6.0(esbuild@0.25.9):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
esbuild: 0.25.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -12095,7 +12148,7 @@ snapshots:
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
eslint: 8.57.1
|
||||
get-tsconfig: 4.10.1
|
||||
is-bun-module: 2.0.0
|
||||
@@ -12217,7 +12270,7 @@ snapshots:
|
||||
|
||||
eslint@8.57.1:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
'@eslint/eslintrc': 2.1.4
|
||||
'@eslint/js': 8.57.1
|
||||
@@ -12228,7 +12281,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
@@ -13601,7 +13654,7 @@ snapshots:
|
||||
micromark@4.0.2:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
decode-named-character-reference: 1.2.0
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
@@ -13737,7 +13790,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@next/env': 15.4.7
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001741
|
||||
caniuse-lite: 1.0.30001735
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -14258,6 +14311,12 @@ snapshots:
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
prism-react-renderer@2.4.1(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/prismjs': 1.26.5
|
||||
clsx: 2.1.1
|
||||
react: 18.3.1
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
@@ -14436,7 +14495,7 @@ snapshots:
|
||||
|
||||
react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@babel/runtime': 7.28.3
|
||||
memoize-one: 5.2.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -14657,7 +14716,7 @@ snapshots:
|
||||
|
||||
require-in-the-middle@7.5.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.1
|
||||
module-details-from-path: 1.0.4
|
||||
resolve: 1.22.10
|
||||
transitivePeerDependencies:
|
||||
@@ -15200,9 +15259,12 @@ snapshots:
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwind-scrollbar@3.1.0(tailwindcss@3.4.17):
|
||||
tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17):
|
||||
dependencies:
|
||||
prism-react-renderer: 2.4.1(react@18.3.1)
|
||||
tailwindcss: 3.4.17
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
|
||||
dependencies:
|
||||
|
||||
@@ -10,9 +10,9 @@ import OnboardingAgentCard from "../components/OnboardingAgentCard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import { finishOnboarding } from "../6-congrats/actions";
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
import { finishOnboarding } from "../6-congrats/actions";
|
||||
|
||||
export default function Page() {
|
||||
const { state, updateState } = useOnboarding(4, "INTEGRATIONS");
|
||||
@@ -24,7 +24,6 @@ export default function Page() {
|
||||
if (agents.length < 2) {
|
||||
finishOnboarding();
|
||||
}
|
||||
|
||||
setAgents(agents);
|
||||
});
|
||||
}, [api, setAgents]);
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
|
||||
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
|
||||
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||
import { useState } from "react";
|
||||
import { getSchemaDefaultCredentials } from "../../helpers";
|
||||
import { areAllCredentialsSet, getCredentialFields } from "./helpers";
|
||||
|
||||
type Credential = CredentialsMetaInput | undefined;
|
||||
type Credentials = Record<string, Credential>;
|
||||
|
||||
type Props = {
|
||||
agent: GraphMeta | null;
|
||||
siblingInputs?: Record<string, any>;
|
||||
onCredentialsChange: (
|
||||
credentials: Record<string, CredentialsMetaInput>,
|
||||
) => void;
|
||||
onValidationChange: (isValid: boolean) => void;
|
||||
onLoadingChange: (isLoading: boolean) => void;
|
||||
};
|
||||
|
||||
export function AgentOnboardingCredentials(props: Props) {
|
||||
const [inputCredentials, setInputCredentials] = useState<Credentials>({});
|
||||
|
||||
const fields = getCredentialFields(props.agent);
|
||||
const required = Object.keys(fields || {}).length > 0;
|
||||
|
||||
if (!required) return null;
|
||||
|
||||
function handleSelectCredentials(key: string, value: Credential) {
|
||||
const updated = { ...inputCredentials, [key]: value };
|
||||
setInputCredentials(updated);
|
||||
|
||||
const sanitized: Record<string, CredentialsMetaInput> = {};
|
||||
for (const [k, v] of Object.entries(updated)) {
|
||||
if (v) sanitized[k] = v;
|
||||
}
|
||||
|
||||
props.onCredentialsChange(sanitized);
|
||||
|
||||
const isValid = !required || areAllCredentialsSet(fields, updated);
|
||||
props.onValidationChange(isValid);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(fields).map(([key, inputSubSchema]) => (
|
||||
<div key={key} className="mt-4">
|
||||
<CredentialsInput
|
||||
schema={inputSubSchema}
|
||||
selectedCredentials={
|
||||
inputCredentials[key] ??
|
||||
getSchemaDefaultCredentials(inputSubSchema)
|
||||
}
|
||||
onSelectCredentials={(value) => handleSelectCredentials(key, value)}
|
||||
siblingInputs={props.siblingInputs}
|
||||
onLoaded={(loaded) => props.onLoadingChange(!loaded)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
|
||||
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export function getCredentialFields(
|
||||
agent: GraphMeta | null,
|
||||
): AgentCredentialsFields {
|
||||
if (!agent) return {};
|
||||
|
||||
const hasNoInputs =
|
||||
!agent.credentials_input_schema ||
|
||||
typeof agent.credentials_input_schema !== "object" ||
|
||||
!("properties" in agent.credentials_input_schema) ||
|
||||
!agent.credentials_input_schema.properties;
|
||||
|
||||
if (hasNoInputs) return {};
|
||||
|
||||
return agent.credentials_input_schema.properties as AgentCredentialsFields;
|
||||
}
|
||||
|
||||
export type AgentCredentialsFields = Record<
|
||||
string,
|
||||
BlockIOCredentialsSubSchema
|
||||
>;
|
||||
|
||||
export function areAllCredentialsSet(
|
||||
fields: AgentCredentialsFields,
|
||||
inputs: Record<string, CredentialsMetaInput | undefined>,
|
||||
) {
|
||||
const required = Object.keys(fields || {});
|
||||
return required.every((k) => Boolean(inputs[k]));
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { OnboardingText } from "../../components/OnboardingText";
|
||||
|
||||
type RunAgentHintProps = {
|
||||
handleNewRun: () => void;
|
||||
};
|
||||
|
||||
export function RunAgentHint(props: RunAgentHintProps) {
|
||||
return (
|
||||
<div className="ml-[104px] w-[481px] pl-5">
|
||||
<div className="flex flex-col">
|
||||
<OnboardingText variant="header">Run your first agent</OnboardingText>
|
||||
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
|
||||
A 'run' is when your agent starts working on a task
|
||||
</span>
|
||||
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
|
||||
Click on <b>New Run</b> below to try it out
|
||||
</span>
|
||||
|
||||
<div
|
||||
onClick={props.handleNewRun}
|
||||
className={cn(
|
||||
"mt-16 flex h-[68px] w-[330px] items-center justify-center rounded-xl border-2 border-violet-700 bg-neutral-50",
|
||||
"cursor-pointer transition-all duration-200 ease-in-out hover:bg-violet-50",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g stroke="#6d28d9" strokeWidth="1.2" strokeLinecap="round">
|
||||
<line x1="16" y1="8" x2="16" y2="24" />
|
||||
<line x1="8" y1="16" x2="24" y2="16" />
|
||||
</g>
|
||||
</svg>
|
||||
<span className="ml-3 font-sans text-[19px] font-medium leading-normal text-violet-700">
|
||||
New run
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import StarRating from "../../components/StarRating";
|
||||
import SmartImage from "@/components/__legacy__/SmartImage";
|
||||
|
||||
type Props = {
|
||||
storeAgent: StoreAgentDetails | null;
|
||||
};
|
||||
|
||||
export function SelectedAgentCard(props: Props) {
|
||||
return (
|
||||
<div className="fixed left-1/4 top-1/2 w-[481px] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="h-[156px] w-[481px] rounded-xl bg-white px-6 pb-5 pt-4">
|
||||
<span className="font-sans text-xs font-medium tracking-wide text-zinc-500">
|
||||
SELECTED AGENT
|
||||
</span>
|
||||
{props.storeAgent ? (
|
||||
<div className="mt-4 flex h-20 rounded-lg bg-violet-50 p-3">
|
||||
{/* Left image */}
|
||||
<SmartImage
|
||||
src={props.storeAgent.agent_image[0]}
|
||||
alt="Agent cover"
|
||||
className="w-[350px] rounded-lg"
|
||||
/>
|
||||
{/* Right content */}
|
||||
<div className="ml-3 flex flex-1 flex-col">
|
||||
<div className="mb-2 flex flex-col items-start">
|
||||
<span className="w-[292px] truncate font-sans text-[14px] font-medium leading-tight text-zinc-800">
|
||||
{props.storeAgent.agent_name}
|
||||
</span>
|
||||
<span className="font-norma w-[292px] truncate font-sans text-xs text-zinc-600">
|
||||
by {props.storeAgent.creator}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex w-[292px] items-center justify-between">
|
||||
<span className="truncate font-sans text-xs font-normal leading-tight text-zinc-600">
|
||||
{props.storeAgent.runs.toLocaleString("en-US")} runs
|
||||
</span>
|
||||
<StarRating
|
||||
className="font-sans text-xs font-normal leading-tight text-zinc-600"
|
||||
starSize={12}
|
||||
rating={props.storeAgent.rating || 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex h-20 animate-pulse rounded-lg bg-gray-300 p-2" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import type {
|
||||
BlockIOCredentialsSubSchema,
|
||||
CredentialsMetaInput,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import type { InputValues } from "./types";
|
||||
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||
|
||||
export function computeInitialAgentInputs(
|
||||
agent: GraphMeta | null,
|
||||
@@ -21,6 +21,7 @@ export function computeInitialAgentInputs(
|
||||
result[key] = existingInputs[key];
|
||||
return;
|
||||
}
|
||||
// GraphIOSubSchema.default is typed as string, but server may return other primitives
|
||||
const def = (subSchema as unknown as { default?: string | number }).default;
|
||||
result[key] = def ?? "";
|
||||
});
|
||||
@@ -28,20 +29,40 @@ export function computeInitialAgentInputs(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getAgentCredentialsInputFields(agent: GraphMeta | null) {
|
||||
const hasNoInputs =
|
||||
!agent?.credentials_input_schema ||
|
||||
typeof agent.credentials_input_schema !== "object" ||
|
||||
!("properties" in agent.credentials_input_schema) ||
|
||||
!agent.credentials_input_schema.properties;
|
||||
|
||||
if (hasNoInputs) return {};
|
||||
|
||||
return agent.credentials_input_schema.properties;
|
||||
}
|
||||
|
||||
export function areAllCredentialsSet(
|
||||
fields: Record<string, BlockIOCredentialsSubSchema>,
|
||||
inputs: Record<string, CredentialsMetaInput | undefined>,
|
||||
) {
|
||||
const required = Object.keys(fields || {});
|
||||
return required.every((k) => Boolean(inputs[k]));
|
||||
}
|
||||
|
||||
type IsRunDisabledParams = {
|
||||
agent: GraphMeta | null;
|
||||
isRunning: boolean;
|
||||
agentInputs: InputValues | null | undefined;
|
||||
credentialsValid: boolean;
|
||||
credentialsLoaded: boolean;
|
||||
credentialsRequired: boolean;
|
||||
credentialsSatisfied: boolean;
|
||||
};
|
||||
|
||||
export function isRunDisabled({
|
||||
agent,
|
||||
isRunning,
|
||||
agentInputs,
|
||||
credentialsValid,
|
||||
credentialsLoaded,
|
||||
credentialsRequired,
|
||||
credentialsSatisfied,
|
||||
}: IsRunDisabledParams) {
|
||||
const hasEmptyInput = Object.values(agentInputs || {}).some(
|
||||
(value) => String(value).trim() === "",
|
||||
@@ -50,8 +71,7 @@ export function isRunDisabled({
|
||||
if (hasEmptyInput) return true;
|
||||
if (!agent) return true;
|
||||
if (isRunning) return true;
|
||||
if (!credentialsValid) return true;
|
||||
if (!credentialsLoaded) return true;
|
||||
if (credentialsRequired && !credentialsSatisfied) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -61,3 +81,13 @@ export function getSchemaDefaultCredentials(
|
||||
): CredentialsMetaInput | undefined {
|
||||
return schema.default as CredentialsMetaInput | undefined;
|
||||
}
|
||||
|
||||
export function sanitizeCredentials(
|
||||
map: Record<string, CredentialsMetaInput | undefined>,
|
||||
): Record<string, CredentialsMetaInput> {
|
||||
const sanitized: Record<string, CredentialsMetaInput> = {};
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
if (value) sanitized[key] = value;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
@@ -1,66 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import SmartImage from "@/components/__legacy__/SmartImage";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
import OnboardingButton from "../components/OnboardingButton";
|
||||
import { OnboardingHeader, OnboardingStep } from "../components/OnboardingStep";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import StarRating from "../components/StarRating";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/__legacy__/ui/card";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { GraphMeta, StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import type { InputValues } from "./types";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Play } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentInputs/RunAgentInputs";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import { isRunDisabled } from "./helpers";
|
||||
import { useOnboardingRunStep } from "./useOnboardingRunStep";
|
||||
import { RunAgentHint } from "./components/RunAgentHint";
|
||||
import { SelectedAgentCard } from "./components/SelectedAgentCard";
|
||||
import { AgentOnboardingCredentials } from "./components/AgentOnboardingCredentials/AgentOnboardingCredentials";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
|
||||
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
areAllCredentialsSet,
|
||||
computeInitialAgentInputs,
|
||||
getAgentCredentialsInputFields,
|
||||
isRunDisabled,
|
||||
getSchemaDefaultCredentials,
|
||||
sanitizeCredentials,
|
||||
} from "./helpers";
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
ready,
|
||||
error,
|
||||
showInput,
|
||||
agent,
|
||||
onboarding,
|
||||
storeAgent,
|
||||
runningAgent,
|
||||
credentialsValid,
|
||||
credentialsLoaded,
|
||||
handleSetAgentInput,
|
||||
handleRunAgent,
|
||||
handleNewRun,
|
||||
handleCredentialsChange,
|
||||
handleCredentialsValidationChange,
|
||||
handleCredentialsLoadingChange,
|
||||
} = useOnboardingRunStep();
|
||||
const { state, updateState, setStep } = useOnboarding(
|
||||
undefined,
|
||||
"AGENT_CHOICE",
|
||||
);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [agent, setAgent] = useState<GraphMeta | null>(null);
|
||||
const [storeAgent, setStoreAgent] = useState<StoreAgentDetails | null>(null);
|
||||
const [runningAgent, setRunningAgent] = useState(false);
|
||||
const [inputCredentials, setInputCredentials] = useState<
|
||||
Record<string, CredentialsMetaInput | undefined>
|
||||
>({});
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
if (error) {
|
||||
return <ErrorCard responseError={error} />;
|
||||
useEffect(() => {
|
||||
setStep(5);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state?.selectedStoreListingVersionId) {
|
||||
return;
|
||||
}
|
||||
api
|
||||
.getStoreAgentByVersionId(state?.selectedStoreListingVersionId)
|
||||
.then((storeAgent) => {
|
||||
setStoreAgent(storeAgent);
|
||||
});
|
||||
api
|
||||
.getGraphMetaByStoreListingVersionID(state.selectedStoreListingVersionId)
|
||||
.then((meta) => {
|
||||
setAgent(meta);
|
||||
const update = computeInitialAgentInputs(
|
||||
meta,
|
||||
(state.agentInput as unknown as InputValues) || null,
|
||||
);
|
||||
updateState({ agentInput: update });
|
||||
});
|
||||
}, [api, setAgent, updateState, state?.selectedStoreListingVersionId]);
|
||||
|
||||
const agentCredentialsInputFields = getAgentCredentialsInputFields(agent);
|
||||
|
||||
const credentialsRequired =
|
||||
Object.keys(agentCredentialsInputFields || {}).length > 0;
|
||||
|
||||
const allCredentialsAreSet = areAllCredentialsSet(
|
||||
agentCredentialsInputFields,
|
||||
inputCredentials,
|
||||
);
|
||||
|
||||
function setAgentInput(key: string, value: string) {
|
||||
updateState({
|
||||
agentInput: {
|
||||
...state?.agentInput,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
async function runAgent() {
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
setRunningAgent(true);
|
||||
try {
|
||||
const libraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||
storeAgent?.store_listing_version_id || "",
|
||||
);
|
||||
const { id: runID } = await api.executeGraph(
|
||||
libraryAgent.graph_id,
|
||||
libraryAgent.graph_version,
|
||||
state?.agentInput || {},
|
||||
sanitizeCredentials(inputCredentials),
|
||||
);
|
||||
updateState({
|
||||
onboardingAgentExecutionId: runID,
|
||||
agentRuns: (state?.agentRuns || 0) + 1,
|
||||
});
|
||||
router.push("/onboarding/6-congrats");
|
||||
} catch (error) {
|
||||
console.error("Error running agent:", error);
|
||||
toast({
|
||||
title: "Error running agent",
|
||||
description:
|
||||
"There was an error running your agent. Please try again or try choosing a different agent if it still fails.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setRunningAgent(false);
|
||||
}
|
||||
}
|
||||
|
||||
const runYourAgent = (
|
||||
<div className="ml-[104px] w-[481px] pl-5">
|
||||
<div className="flex flex-col">
|
||||
<OnboardingText variant="header">Run your first agent</OnboardingText>
|
||||
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
|
||||
A 'run' is when your agent starts working on a task
|
||||
</span>
|
||||
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
|
||||
Click on <b>New Run</b> below to try it out
|
||||
</span>
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
setShowInput(true);
|
||||
setStep(6);
|
||||
updateState({
|
||||
completedSteps: [
|
||||
...(state?.completedSteps || []),
|
||||
"AGENT_NEW_RUN",
|
||||
],
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"mt-16 flex h-[68px] w-[330px] items-center justify-center rounded-xl border-2 border-violet-700 bg-neutral-50",
|
||||
"cursor-pointer transition-all duration-200 ease-in-out hover:bg-violet-50",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g stroke="#6d28d9" strokeWidth="1.2" strokeLinecap="round">
|
||||
<line x1="16" y1="8" x2="16" y2="24" />
|
||||
<line x1="8" y1="16" x2="24" y2="16" />
|
||||
</g>
|
||||
</svg>
|
||||
<span className="ml-3 font-sans text-[19px] font-medium leading-normal text-violet-700">
|
||||
New run
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingStep dotted>
|
||||
<OnboardingHeader backHref={"/onboarding/4-agent"} transparent />
|
||||
{/* Agent card */}
|
||||
<div className="fixed left-1/4 top-1/2 w-[481px] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="h-[156px] w-[481px] rounded-xl bg-white px-6 pb-5 pt-4">
|
||||
<span className="font-sans text-xs font-medium tracking-wide text-zinc-500">
|
||||
SELECTED AGENT
|
||||
</span>
|
||||
{storeAgent ? (
|
||||
<div className="mt-4 flex h-20 rounded-lg bg-violet-50 p-2">
|
||||
{/* Left image */}
|
||||
<SmartImage
|
||||
src={storeAgent?.agent_image[0]}
|
||||
alt="Agent cover"
|
||||
imageContain
|
||||
className="w-[350px] rounded-lg"
|
||||
/>
|
||||
{/* Right content */}
|
||||
<div className="ml-2 flex flex-1 flex-col">
|
||||
<span className="w-[292px] truncate font-sans text-[14px] font-medium leading-normal text-zinc-800">
|
||||
{storeAgent?.agent_name}
|
||||
</span>
|
||||
<span className="mt-[5px] w-[292px] truncate font-sans text-xs font-normal leading-tight text-zinc-600">
|
||||
by {storeAgent?.creator}
|
||||
</span>
|
||||
<div className="mt-auto flex w-[292px] justify-between">
|
||||
<span className="mt-1 truncate font-sans text-xs font-normal leading-tight text-zinc-600">
|
||||
{storeAgent?.runs.toLocaleString("en-US")} runs
|
||||
</span>
|
||||
<StarRating
|
||||
className="font-sans text-xs font-normal leading-tight text-zinc-600"
|
||||
starSize={12}
|
||||
rating={storeAgent?.rating || 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex h-20 animate-pulse rounded-lg bg-gray-300 p-2" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-[80vh] items-center justify-center">
|
||||
<SelectedAgentCard storeAgent={storeAgent} />
|
||||
{/* Left side */}
|
||||
<div className="w-[481px]" />
|
||||
{/* Right side */}
|
||||
{!showInput ? (
|
||||
<RunAgentHint handleNewRun={handleNewRun} />
|
||||
runYourAgent
|
||||
) : (
|
||||
<div className="ml-[104px] w-[481px] pl-5">
|
||||
<div className="flex flex-col">
|
||||
@@ -74,7 +232,30 @@ export default function Page() {
|
||||
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
|
||||
When you're done, click <b>Run Agent</b>.
|
||||
</span>
|
||||
|
||||
{Object.entries(agentCredentialsInputFields || {}).map(
|
||||
([key, inputSubSchema]) => (
|
||||
<div key={key} className="mt-4">
|
||||
<CredentialsInput
|
||||
schema={inputSubSchema}
|
||||
selectedCredentials={
|
||||
inputCredentials[key] ??
|
||||
getSchemaDefaultCredentials(inputSubSchema)
|
||||
}
|
||||
onSelectCredentials={(value) =>
|
||||
setInputCredentials((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}))
|
||||
}
|
||||
siblingInputs={
|
||||
(state?.agentInput || undefined) as
|
||||
| Record<string, any>
|
||||
| undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<Card className="agpt-box mt-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-lg">Input</CardTitle>
|
||||
@@ -91,23 +272,13 @@ export default function Page() {
|
||||
</label>
|
||||
<RunAgentInputs
|
||||
schema={inputSubSchema}
|
||||
value={onboarding.state?.agentInput?.[key]}
|
||||
value={state?.agentInput?.[key]}
|
||||
placeholder={inputSubSchema.description}
|
||||
onChange={(value) => handleSetAgentInput(key, value)}
|
||||
onChange={(value) => setAgentInput(key, value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<AgentOnboardingCredentials
|
||||
agent={agent}
|
||||
siblingInputs={
|
||||
(onboarding.state?.agentInput as Record<string, any>) ||
|
||||
undefined
|
||||
}
|
||||
onCredentialsChange={handleCredentialsChange}
|
||||
onValidationChange={handleCredentialsValidationChange}
|
||||
onLoadingChange={handleCredentialsLoadingChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<OnboardingButton
|
||||
@@ -118,12 +289,11 @@ export default function Page() {
|
||||
agent,
|
||||
isRunning: runningAgent,
|
||||
agentInputs:
|
||||
(onboarding.state?.agentInput as unknown as InputValues) ||
|
||||
null,
|
||||
credentialsValid,
|
||||
credentialsLoaded,
|
||||
(state?.agentInput as unknown as InputValues) || null,
|
||||
credentialsRequired,
|
||||
credentialsSatisfied: allCredentialsAreSet,
|
||||
})}
|
||||
onClick={handleRunAgent}
|
||||
onClick={runAgent}
|
||||
icon={<Play className="mr-2" size={18} />}
|
||||
>
|
||||
Run agent
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
|
||||
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { computeInitialAgentInputs } from "./helpers";
|
||||
import { InputValues } from "./types";
|
||||
import {
|
||||
useGetV2GetAgentByVersion,
|
||||
useGetV2GetAgentGraph,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
|
||||
export function useOnboardingRunStep() {
|
||||
const onboarding = useOnboarding(undefined, "AGENT_CHOICE");
|
||||
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [agent, setAgent] = useState<GraphMeta | null>(null);
|
||||
const [storeAgent, setStoreAgent] = useState<StoreAgentDetails | null>(null);
|
||||
const [runningAgent, setRunningAgent] = useState(false);
|
||||
|
||||
const [inputCredentials, setInputCredentials] = useState<
|
||||
Record<string, CredentialsMetaInput>
|
||||
>({});
|
||||
|
||||
const [credentialsValid, setCredentialsValid] = useState(true);
|
||||
const [credentialsLoaded, setCredentialsLoaded] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
const currentAgentVersion =
|
||||
onboarding.state?.selectedStoreListingVersionId ?? "";
|
||||
|
||||
const storeAgentQuery = useGetV2GetAgentByVersion(currentAgentVersion, {
|
||||
query: { enabled: !!currentAgentVersion },
|
||||
});
|
||||
|
||||
const graphMetaQuery = useGetV2GetAgentGraph(currentAgentVersion, {
|
||||
query: { enabled: !!currentAgentVersion },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onboarding.setStep(5);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (storeAgentQuery.data && storeAgentQuery.data.status === 200) {
|
||||
setStoreAgent(storeAgentQuery.data.data);
|
||||
}
|
||||
}, [storeAgentQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
graphMetaQuery.data &&
|
||||
graphMetaQuery.data.status === 200 &&
|
||||
onboarding.state
|
||||
) {
|
||||
const graphMeta = graphMetaQuery.data.data as GraphMeta;
|
||||
|
||||
setAgent(graphMeta);
|
||||
|
||||
const update = computeInitialAgentInputs(
|
||||
graphMeta,
|
||||
(onboarding.state.agentInput as unknown as InputValues) || null,
|
||||
);
|
||||
|
||||
onboarding.updateState({ agentInput: update });
|
||||
}
|
||||
}, [graphMetaQuery.data]);
|
||||
|
||||
function handleNewRun() {
|
||||
if (!onboarding.state) return;
|
||||
|
||||
setShowInput(true);
|
||||
onboarding.setStep(6);
|
||||
onboarding.updateState({
|
||||
completedSteps: [
|
||||
...(onboarding.state.completedSteps || []),
|
||||
"AGENT_NEW_RUN",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function handleSetAgentInput(key: string, value: string) {
|
||||
if (!onboarding.state) return;
|
||||
|
||||
onboarding.updateState({
|
||||
agentInput: {
|
||||
...onboarding.state.agentInput,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRunAgent() {
|
||||
if (!agent || !storeAgent || !onboarding.state) {
|
||||
toast({
|
||||
title: "Error getting agent",
|
||||
description:
|
||||
"Either the agent is not available or there was an error getting it.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setRunningAgent(true);
|
||||
|
||||
try {
|
||||
const libraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||
storeAgent?.store_listing_version_id || "",
|
||||
);
|
||||
|
||||
const { id: runID } = await api.executeGraph(
|
||||
libraryAgent.graph_id,
|
||||
libraryAgent.graph_version,
|
||||
onboarding.state.agentInput || {},
|
||||
inputCredentials,
|
||||
);
|
||||
|
||||
onboarding.updateState({
|
||||
onboardingAgentExecutionId: runID,
|
||||
agentRuns: (onboarding.state.agentRuns || 0) + 1,
|
||||
});
|
||||
|
||||
router.push("/onboarding/6-congrats");
|
||||
} catch (error) {
|
||||
console.error("Error running agent:", error);
|
||||
|
||||
toast({
|
||||
title: "Error running agent",
|
||||
description:
|
||||
"There was an error running your agent. Please try again or try choosing a different agent if it still fails.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
setRunningAgent(false);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ready: graphMetaQuery.isSuccess && storeAgentQuery.isSuccess,
|
||||
error: graphMetaQuery.error || storeAgentQuery.error,
|
||||
agent,
|
||||
onboarding,
|
||||
showInput,
|
||||
storeAgent,
|
||||
runningAgent,
|
||||
credentialsValid,
|
||||
credentialsLoaded,
|
||||
handleSetAgentInput,
|
||||
handleRunAgent,
|
||||
handleNewRun,
|
||||
handleCredentialsChange: setInputCredentials,
|
||||
handleCredentialsValidationChange: setCredentialsValid,
|
||||
handleCredentialsLoadingChange: (v: boolean) => setCredentialsLoaded(!v),
|
||||
};
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export default function StarRating({
|
||||
)}
|
||||
>
|
||||
{/* Display numerical rating */}
|
||||
<span className="mr-1 mt-0.5">{roundedRating}</span>
|
||||
<span className="mr-1 mt-1">{roundedRating}</span>
|
||||
|
||||
{/* Display stars */}
|
||||
{stars.map((starType, index) => {
|
||||
|
||||
@@ -44,7 +44,10 @@ export default function AuthErrorPage() {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Card className="w-full max-w-md p-8">
|
||||
<WaitlistErrorContent onBackToLogin={() => router.push("/login")} />
|
||||
<WaitlistErrorContent
|
||||
onClose={() => router.push("/login")}
|
||||
closeButtonText="Back to Login"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { RunGraph } from "./components/RunGraph";
|
||||
|
||||
export const BuilderActions = () => {
|
||||
return (
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] -translate-x-1/2">
|
||||
{/* TODO: Add Agent Output */}
|
||||
<RunGraph />
|
||||
{/* TODO: Add Schedule run button */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlayIcon } from "lucide-react";
|
||||
import { useRunGraph } from "./useRunGraph";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { StopIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const RunGraph = () => {
|
||||
const { runGraph, isSaving } = useRunGraph();
|
||||
const isGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.isGraphRunning),
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className={cn(
|
||||
"relative min-w-44 border-none bg-gradient-to-r from-purple-500 to-pink-500 text-lg",
|
||||
)}
|
||||
onClick={() => runGraph()}
|
||||
>
|
||||
{!isGraphRunning && !isSaving ? (
|
||||
<PlayIcon className="mr-1 size-5" />
|
||||
) : (
|
||||
<StopIcon className="mr-1 size-5" />
|
||||
)}
|
||||
{isGraphRunning || isSaving ? "Stop Agent" : "Run Agent"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useNewSaveControl } from "../../../NewControlPanel/NewSaveControl/useNewSaveControl";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { GraphExecutionMeta } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/use-agent-runs";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const useRunGraph = () => {
|
||||
const { onSubmit: onSaveGraph, isLoading: isSaving } = useNewSaveControl({
|
||||
showToast: false,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
flowExecutionID: parseAsString,
|
||||
});
|
||||
|
||||
const { mutateAsync: executeGraph } = usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const { id } = response.data as GraphExecutionMeta;
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const runGraph = async () => {
|
||||
setIsGraphRunning(true);
|
||||
await onSaveGraph(undefined);
|
||||
|
||||
// Todo : We need to save graph which has inputs and credentials inputs
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: {
|
||||
inputs: {},
|
||||
credentials_inputs: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
runGraph,
|
||||
isSaving,
|
||||
};
|
||||
};
|
||||
@@ -7,11 +7,7 @@ import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useMemo } from "react";
|
||||
import { CustomNode } from "../nodes/CustomNode/CustomNode";
|
||||
import { useCustomEdge } from "../edges/useCustomEdge";
|
||||
import { useFlowRealtime } from "./useFlowRealtime";
|
||||
import { GraphLoadingBox } from "./components/GraphLoadingBox";
|
||||
import { BuilderActions } from "../BuilderActions/BuilderActions";
|
||||
import { RunningBackground } from "./components/RunningBackground";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
import { GraphLoadingBox } from "./GraphLoadingBox";
|
||||
|
||||
export const Flow = () => {
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
@@ -22,11 +18,8 @@ export const Flow = () => {
|
||||
const { edges, onConnect, onEdgesChange } = useCustomEdge();
|
||||
|
||||
// We use this hook to load the graph and convert them into custom nodes and edges.
|
||||
useFlow();
|
||||
useFlowRealtime();
|
||||
|
||||
const { isFlowContentLoading } = useFlow();
|
||||
const { isGraphRunning } = useGraphStore();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full dark:bg-slate-900">
|
||||
<div className="relative flex-1">
|
||||
@@ -44,9 +37,7 @@ export const Flow = () => {
|
||||
<Background />
|
||||
<Controls />
|
||||
<NewControlPanel />
|
||||
<BuilderActions />
|
||||
{isFlowContentLoading && <GraphLoadingBox />}
|
||||
{isGraphRunning && <RunningBackground />}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ export const GraphLoadingBox = () => {
|
||||
<div className="absolute left-[50%] top-[50%] z-[99] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="flex flex-col items-center gap-4 rounded-xlarge border border-gray-200 bg-white p-8 shadow-lg dark:border-gray-700 dark:bg-slate-800">
|
||||
<div className="relative h-12 w-12">
|
||||
<div className="absolute inset-0 animate-spin rounded-full border-4 border-violet-200 border-t-violet-500 dark:border-gray-700 dark:border-t-blue-400"></div>
|
||||
<div className="absolute inset-0 animate-spin rounded-full border-4 border-gray-200 border-t-black dark:border-gray-700 dark:border-t-blue-400"></div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Text variant="h4">Loading Flow</Text>
|
||||
@@ -1,157 +0,0 @@
|
||||
export const RunningBackground = () => {
|
||||
return (
|
||||
<div className="absolute inset-0 h-full w-full">
|
||||
<style jsx>{`
|
||||
@keyframes rotateGradient {
|
||||
0% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#bc82f3 17%,
|
||||
#f5b9ea 24%,
|
||||
#8d99ff 35%,
|
||||
#aa6eee 58%,
|
||||
#ff6778 70%,
|
||||
#ffba71 81%,
|
||||
#c686ff 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
14.28% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#c686ff 17%,
|
||||
#bc82f3 24%,
|
||||
#f5b9ea 35%,
|
||||
#8d99ff 58%,
|
||||
#aa6eee 70%,
|
||||
#ff6778 81%,
|
||||
#ffba71 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
28.56% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#ffba71 17%,
|
||||
#c686ff 24%,
|
||||
#bc82f3 35%,
|
||||
#f5b9ea 58%,
|
||||
#8d99ff 70%,
|
||||
#aa6eee 81%,
|
||||
#ff6778 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
42.84% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#ff6778 17%,
|
||||
#ffba71 24%,
|
||||
#c686ff 35%,
|
||||
#bc82f3 58%,
|
||||
#f5b9ea 70%,
|
||||
#8d99ff 81%,
|
||||
#aa6eee 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
57.12% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#aa6eee 17%,
|
||||
#ff6778 24%,
|
||||
#ffba71 35%,
|
||||
#c686ff 58%,
|
||||
#bc82f3 70%,
|
||||
#f5b9ea 81%,
|
||||
#8d99ff 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
71.4% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#8d99ff 17%,
|
||||
#aa6eee 24%,
|
||||
#ff6778 35%,
|
||||
#ffba71 58%,
|
||||
#c686ff 70%,
|
||||
#bc82f3 81%,
|
||||
#f5b9ea 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
85.68% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#f5b9ea 17%,
|
||||
#8d99ff 24%,
|
||||
#aa6eee 35%,
|
||||
#ff6778 58%,
|
||||
#ffba71 70%,
|
||||
#c686ff 81%,
|
||||
#bc82f3 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
100% {
|
||||
border-image: linear-gradient(
|
||||
to right,
|
||||
#bc82f3 17%,
|
||||
#f5b9ea 24%,
|
||||
#8d99ff 35%,
|
||||
#aa6eee 58%,
|
||||
#ff6778 70%,
|
||||
#ffba71 81%,
|
||||
#c686ff 92%
|
||||
)
|
||||
1;
|
||||
}
|
||||
}
|
||||
.animate-gradient {
|
||||
animation: rotateGradient 8s linear infinite;
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-xl"
|
||||
style={{
|
||||
borderWidth: "15px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-lg"
|
||||
style={{
|
||||
borderWidth: "10px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-md"
|
||||
style={{
|
||||
borderWidth: "6px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="animate-gradient absolute inset-0 bg-transparent blur-sm"
|
||||
style={{
|
||||
borderWidth: "6px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
borderImage:
|
||||
"linear-gradient(to right, #BC82F3 17%, #F5B9EA 24%, #8D99FF 35%, #AA6EEE 58%, #FF6778 70%, #FFBA71 81%, #C686FF 92%) 1",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/default/default";
|
||||
import {
|
||||
useGetV1GetExecutionDetails,
|
||||
useGetV1GetSpecificGraph,
|
||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
@@ -11,39 +8,16 @@ import { useShallow } from "zustand/react/shallow";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { convertNodesPlusBlockInfoIntoCustomNodes } from "../../helper";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
|
||||
export const useFlow = () => {
|
||||
const addNodes = useNodeStore(useShallow((state) => state.addNodes));
|
||||
const addLinks = useEdgeStore(useShallow((state) => state.addLinks));
|
||||
const updateNodeStatus = useNodeStore(
|
||||
useShallow((state) => state.updateNodeStatus),
|
||||
);
|
||||
const updateNodeExecutionResult = useNodeStore(
|
||||
useShallow((state) => state.updateNodeExecutionResult),
|
||||
);
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
const [{ flowID, flowVersion, flowExecutionID }] = useQueryStates({
|
||||
|
||||
const [{ flowID, flowVersion }] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
flowExecutionID: parseAsString,
|
||||
});
|
||||
|
||||
const { data: executionDetails } = useGetV1GetExecutionDetails(
|
||||
flowID || "",
|
||||
flowExecutionID || "",
|
||||
{
|
||||
query: {
|
||||
select: (res) => res.data as GetV1GetExecutionDetails200,
|
||||
enabled: !!flowID && !!flowExecutionID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
|
||||
flowID ?? "",
|
||||
flowVersion !== null ? { version: flowVersion } : {},
|
||||
@@ -83,52 +57,21 @@ export const useFlow = () => {
|
||||
}, [nodes, blocks]);
|
||||
|
||||
useEffect(() => {
|
||||
// adding nodes
|
||||
if (customNodes.length > 0) {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
addNodes(customNodes);
|
||||
}
|
||||
|
||||
// adding links
|
||||
if (graph?.links) {
|
||||
useEdgeStore.getState().setConnections([]);
|
||||
addLinks(graph.links);
|
||||
}
|
||||
|
||||
// update graph running status
|
||||
const isRunning =
|
||||
executionDetails?.status === AgentExecutionStatus.RUNNING ||
|
||||
executionDetails?.status === AgentExecutionStatus.QUEUED;
|
||||
setIsGraphRunning(isRunning);
|
||||
|
||||
// update node execution status in nodes
|
||||
if (
|
||||
executionDetails &&
|
||||
"node_executions" in executionDetails &&
|
||||
executionDetails.node_executions
|
||||
) {
|
||||
executionDetails.node_executions.forEach((nodeExecution) => {
|
||||
updateNodeStatus(nodeExecution.node_id, nodeExecution.status);
|
||||
});
|
||||
}
|
||||
|
||||
// update node execution results in nodes
|
||||
if (
|
||||
executionDetails &&
|
||||
"node_executions" in executionDetails &&
|
||||
executionDetails.node_executions
|
||||
) {
|
||||
executionDetails.node_executions.forEach((nodeExecution) => {
|
||||
updateNodeExecutionResult(nodeExecution.node_id, nodeExecution);
|
||||
});
|
||||
}
|
||||
}, [customNodes, addNodes, graph?.links, executionDetails, updateNodeStatus]);
|
||||
}, [customNodes, addNodes, graph?.links]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
useEdgeStore.getState().setConnections([]);
|
||||
setIsGraphRunning(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// In this hook, I am only keeping websocket related code.
|
||||
|
||||
import { GraphExecutionID } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { parseAsString, useQueryStates } from "nuqs";
|
||||
import { useEffect } from "react";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
|
||||
export const useFlowRealtime = () => {
|
||||
const api = useBackendAPI();
|
||||
const updateNodeExecutionResult = useNodeStore(
|
||||
useShallow((state) => state.updateNodeExecutionResult),
|
||||
);
|
||||
const updateStatus = useNodeStore(
|
||||
useShallow((state) => state.updateNodeStatus),
|
||||
);
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
|
||||
const [{ flowExecutionID, flowID }] = useQueryStates({
|
||||
flowExecutionID: parseAsString,
|
||||
flowID: parseAsString,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const deregisterNodeExecutionEvent = api.onWebSocketMessage(
|
||||
"node_execution_event",
|
||||
(data) => {
|
||||
if (data.graph_exec_id != flowExecutionID) {
|
||||
return;
|
||||
}
|
||||
// TODO: Update the states of nodes
|
||||
updateNodeExecutionResult(
|
||||
data.node_id,
|
||||
data as unknown as NodeExecutionResult,
|
||||
);
|
||||
updateStatus(data.node_id, data.status);
|
||||
},
|
||||
);
|
||||
|
||||
const deregisterGraphExecutionStatusEvent = api.onWebSocketMessage(
|
||||
"graph_execution_event",
|
||||
(graphExecution) => {
|
||||
if (graphExecution.id != flowExecutionID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRunning =
|
||||
graphExecution.status === AgentExecutionStatus.RUNNING ||
|
||||
graphExecution.status === AgentExecutionStatus.QUEUED;
|
||||
|
||||
setIsGraphRunning(isRunning);
|
||||
},
|
||||
);
|
||||
|
||||
const deregisterGraphExecutionSubscription =
|
||||
flowID && flowExecutionID
|
||||
? api.onWebSocketConnect(() => {
|
||||
// Subscribe to execution updates
|
||||
api
|
||||
.subscribeToGraphExecution(flowExecutionID as GraphExecutionID) // TODO: We are currently using a manual type, we need to fix it in future
|
||||
.then(() => {
|
||||
console.debug(
|
||||
`Subscribed to updates for execution #${flowExecutionID}`,
|
||||
);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
`Failed to subscribe to updates for execution #${flowExecutionID}:`,
|
||||
error,
|
||||
),
|
||||
);
|
||||
})
|
||||
: () => {};
|
||||
|
||||
return () => {
|
||||
deregisterNodeExecutionEvent();
|
||||
deregisterGraphExecutionSubscription();
|
||||
deregisterGraphExecutionStatusEvent();
|
||||
};
|
||||
}, [api, flowExecutionID]);
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -147,7 +147,6 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="min-w-10"
|
||||
onClick={() => removeProperty(key)}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -6,8 +6,6 @@ import { StickyNoteBlock } from "./StickyNoteBlock";
|
||||
import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem";
|
||||
import { StandardNodeBlock } from "./StandardNodeBlock";
|
||||
import { BlockCost } from "@/app/api/__generated__/models/blockCost";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
|
||||
export type CustomNodeData = {
|
||||
hardcodedValues: {
|
||||
@@ -19,8 +17,6 @@ export type CustomNodeData = {
|
||||
outputSchema: RJSFSchema;
|
||||
uiType: BlockUIType;
|
||||
block_id: string;
|
||||
status?: AgentExecutionStatus;
|
||||
nodeExecutionResult?: NodeExecutionResult;
|
||||
// TODO : We need better type safety for the following backend fields.
|
||||
costs: BlockCost[];
|
||||
categories: BlockInfoCategoriesItem[];
|
||||
|
||||
@@ -8,9 +8,6 @@ import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
import { NodeCost } from "./components/NodeCost";
|
||||
import { NodeBadges } from "./components/NodeBadges";
|
||||
import { NodeExecutionBadge } from "./components/NodeExecutionBadge";
|
||||
import { nodeStyleBasedOnStatus } from "./helpers";
|
||||
import { NodeDataRenderer } from "./components/NodeDataRenderer";
|
||||
|
||||
type StandardNodeBlockType = {
|
||||
data: CustomNodeData;
|
||||
@@ -26,60 +23,57 @@ export const StandardNodeBlock = ({
|
||||
(state) => state.nodeAdvancedStates[nodeId] || false,
|
||||
);
|
||||
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
|
||||
const status = useNodeStore((state) => state.getNodeStatus(nodeId));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-12 max-w-[370px] rounded-xl shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
|
||||
"z-12 rounded-xl bg-gradient-to-br from-white to-slate-50/30 shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
|
||||
selected && "shadow-2xl ring-2 ring-slate-200",
|
||||
status && nodeStyleBasedOnStatus[status],
|
||||
)}
|
||||
>
|
||||
<div className="rounded-xl bg-white">
|
||||
{/* Header */}
|
||||
<div className="flex h-auto flex-col gap-2 rounded-xl border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4">
|
||||
{/* Upper section */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="large-semibold"
|
||||
className="tracking-tight text-slate-800"
|
||||
>
|
||||
{beautifyString(data.title)}
|
||||
</Text>
|
||||
<Text variant="small" className="!font-medium !text-slate-500">
|
||||
#{nodeId.split("-")[0]}
|
||||
</Text>
|
||||
</div>
|
||||
{/* Lower section */}
|
||||
<div className="flex space-x-2">
|
||||
<NodeCost blockCosts={data.costs} nodeId={nodeId} />
|
||||
<NodeBadges categories={data.categories} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Input Handles */}
|
||||
<div className="bg-white pb-6 pr-6">
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(data.inputSchema)}
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
/>
|
||||
</div>
|
||||
{/* Advanced Button */}
|
||||
<div className="flex items-center justify-between gap-2 border-t border-slate-200/50 bg-white px-5 py-3.5">
|
||||
<Text variant="body" className="font-medium text-slate-700">
|
||||
Advanced
|
||||
{/* Header */}
|
||||
<div className="flex h-auto flex-col gap-2 rounded-xl border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4">
|
||||
{/* Upper section */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="large-semibold"
|
||||
className="tracking-tight text-slate-800"
|
||||
>
|
||||
{beautifyString(data.title)}
|
||||
</Text>
|
||||
<Text variant="small" className="!font-medium !text-slate-500">
|
||||
#{nodeId.split("-")[0]}
|
||||
</Text>
|
||||
<Switch
|
||||
onCheckedChange={(checked) => setShowAdvanced(nodeId, checked)}
|
||||
checked={showAdvanced}
|
||||
/>
|
||||
</div>
|
||||
{/* Output Handles */}
|
||||
<OutputHandler outputSchema={data.outputSchema} nodeId={nodeId} />
|
||||
|
||||
<NodeDataRenderer nodeId={nodeId} />
|
||||
{/* Lower section */}
|
||||
<div className="flex space-x-2">
|
||||
<NodeCost blockCosts={data.costs} nodeId={nodeId} />
|
||||
<NodeBadges categories={data.categories} />
|
||||
</div>
|
||||
</div>
|
||||
{status && <NodeExecutionBadge status={status} />}
|
||||
|
||||
{/* Input Handles */}
|
||||
<div className="bg-white/40 pb-6 pr-6">
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(data.inputSchema)}
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Advanced Button */}
|
||||
<div className="flex items-center justify-between gap-2 rounded-b-xl border-t border-slate-200/50 bg-gradient-to-r from-slate-50/60 to-white/80 px-5 py-3.5">
|
||||
<Text variant="body" className="font-medium text-slate-700">
|
||||
Advanced
|
||||
</Text>
|
||||
<Switch
|
||||
onCheckedChange={(checked) => setShowAdvanced(nodeId, checked)}
|
||||
checked={showAdvanced}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Output Handles */}
|
||||
<OutputHandler outputSchema={data.outputSchema} nodeId={nodeId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export const StickyNoteBlock = ({ data, id }: StickyNoteBlockType) => {
|
||||
style={{ transform: `rotate(${angle}deg)` }}
|
||||
>
|
||||
<Text variant="h3" className="tracking-tight text-slate-800">
|
||||
Notes #{id.split("-")[0]}
|
||||
Notes #{id}
|
||||
</Text>
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(data.inputSchema)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import useCredits from "@/hooks/useCredits";
|
||||
import { CoinIcon } from "@phosphor-icons/react";
|
||||
import { isCostFilterMatch } from "../../../../helper";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const NodeCost = ({
|
||||
blockCosts,
|
||||
@@ -14,10 +13,9 @@ export const NodeCost = ({
|
||||
nodeId: string;
|
||||
}) => {
|
||||
const { formatCredits } = useCredits();
|
||||
const hardcodedValues = useNodeStore(
|
||||
useShallow((state) => state.getHardCodedValues(nodeId)),
|
||||
const hardcodedValues = useNodeStore((state) =>
|
||||
state.getHardCodedValues(nodeId),
|
||||
);
|
||||
|
||||
const blockCost =
|
||||
blockCosts &&
|
||||
blockCosts.find((cost) =>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import {
|
||||
ArrowSquareInIcon,
|
||||
CaretDownIcon,
|
||||
CopyIcon,
|
||||
InfoIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
const nodeExecutionResult = useNodeStore(
|
||||
useShallow((state) => state.getNodeExecutionResult(nodeId)),
|
||||
);
|
||||
|
||||
const data = {
|
||||
"[Input]": nodeExecutionResult?.input_data,
|
||||
...nodeExecutionResult?.output_data,
|
||||
};
|
||||
|
||||
// Don't render if there's no data
|
||||
if (!nodeExecutionResult || Object.keys(data).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Need to Fix - when we are on build page and try to rerun the graph again, it gives error
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-b-xl border-t border-slate-200/50 px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text variant="body-medium" className="!font-semibold text-slate-700">
|
||||
Node Output
|
||||
</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="h-fit min-w-0 p-1 text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
<CaretDownIcon
|
||||
size={16}
|
||||
weight="bold"
|
||||
className={`transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<>
|
||||
<div className="flex max-w-[350px] flex-col gap-4">
|
||||
{Object.entries(data || {}).map(([key, value]) => (
|
||||
<div key={key} className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="!font-semibold text-slate-600"
|
||||
>
|
||||
Pin:
|
||||
</Text>
|
||||
<Text variant="body" className="text-slate-700">
|
||||
{beautifyString(key)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="w-full space-y-2">
|
||||
<Text
|
||||
variant="small"
|
||||
className="!font-semibold text-slate-600"
|
||||
>
|
||||
Data:
|
||||
</Text>
|
||||
<div className="relative">
|
||||
<Text
|
||||
variant="small"
|
||||
className="rounded-xlarge bg-zinc-50 p-3 text-slate-700"
|
||||
>
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</Text>
|
||||
<div className="mt-1 flex justify-end gap-1">
|
||||
{/* TODO: Add tooltip for each button and also make all these blocks working */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="h-fit min-w-0 gap-1.5 p-2 text-black hover:text-slate-900"
|
||||
>
|
||||
<InfoIcon size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="h-fit min-w-0 gap-1.5 p-2 text-black hover:text-slate-900"
|
||||
>
|
||||
<ArrowSquareInIcon size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="h-fit min-w-0 gap-1.5 p-2 text-black hover:text-slate-900"
|
||||
>
|
||||
<CopyIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* TODO: Currently this button is not working, need to make it working */}
|
||||
<Button variant="outline" size="small" className="w-fit self-start">
|
||||
View More
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { Badge } from "@/components/__legacy__/ui/badge";
|
||||
import { LoadingSpinner } from "@/components/__legacy__/ui/loading";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const statusStyles: Record<AgentExecutionStatus, string> = {
|
||||
INCOMPLETE: "text-slate-700 border-slate-400",
|
||||
QUEUED: "text-blue-700 border-blue-400",
|
||||
RUNNING: "text-amber-700 border-amber-400",
|
||||
COMPLETED: "text-green-700 border-green-400",
|
||||
TERMINATED: "text-orange-700 border-orange-400",
|
||||
FAILED: "text-red-700 border-red-400",
|
||||
};
|
||||
|
||||
export const NodeExecutionBadge = ({
|
||||
status,
|
||||
}: {
|
||||
status: AgentExecutionStatus;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-end rounded-b-xl py-2 pr-4">
|
||||
<Badge
|
||||
className={cn(statusStyles[status], "gap-2 rounded-full bg-white")}
|
||||
>
|
||||
{status}
|
||||
{status === AgentExecutionStatus.RUNNING && (
|
||||
<LoadingSpinner className="size-4" />
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
|
||||
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
||||
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
||||
QUEUED: " ring-blue-300 bg-blue-300",
|
||||
RUNNING: "ring-amber-300 bg-amber-300",
|
||||
COMPLETED: "ring-green-300 bg-green-300",
|
||||
TERMINATED: "ring-orange-300 bg-orange-300 ",
|
||||
FAILED: "ring-red-300 bg-red-300",
|
||||
};
|
||||
@@ -35,7 +35,7 @@ export const OutputHandler = ({
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
className="flex items-center gap-2 !font-semibold text-slate-700"
|
||||
className="flex items-center gap-2 font-medium text-slate-700"
|
||||
>
|
||||
Output{" "}
|
||||
<CaretDownIcon
|
||||
|
||||
@@ -79,7 +79,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-[350px] space-y-1 pt-4">
|
||||
<div className="mt-4 w-[350px] space-y-1">
|
||||
{label && schema.type && (
|
||||
<label htmlFor={fieldId} className="flex items-center gap-1">
|
||||
{!suppressHandle && !fromAnyOf && !isCredential && (
|
||||
|
||||
@@ -19,9 +19,7 @@ import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
|
||||
export const NewSaveControl = () => {
|
||||
const { form, onSubmit, isLoading, graphVersion } = useNewSaveControl({
|
||||
showToast: true,
|
||||
});
|
||||
const { form, onSubmit, isLoading, graphVersion } = useNewSaveControl();
|
||||
const { saveControlOpen, setSaveControlOpen } = useControlPanelStore();
|
||||
return (
|
||||
<Popover onOpenChange={setSaveControlOpen}>
|
||||
@@ -113,7 +111,6 @@ export const NewSaveControl = () => {
|
||||
data-id="save-control-save-agent"
|
||||
data-testid="save-control-save-agent-button"
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
Save Agent
|
||||
</Button>
|
||||
|
||||
@@ -25,11 +25,7 @@ const formSchema = z.object({
|
||||
|
||||
type SaveableGraphFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export const useNewSaveControl = ({
|
||||
showToast = true,
|
||||
}: {
|
||||
showToast?: boolean;
|
||||
}) => {
|
||||
export const useNewSaveControl = () => {
|
||||
const { setSaveControlOpen } = useControlPanelStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -64,11 +60,9 @@ export const useNewSaveControl = ({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
if (showToast) {
|
||||
toast({
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
@@ -94,11 +88,9 @@ export const useNewSaveControl = ({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
if (showToast) {
|
||||
toast({
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: "All changes saved successfully!",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1GetSpecificGraphQueryKey(data.id),
|
||||
});
|
||||
@@ -121,41 +113,6 @@ export const useNewSaveControl = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: SaveableGraphFormValues | undefined) => {
|
||||
const graphNodes = useNodeStore.getState().getBackendNodes();
|
||||
const graphLinks = useEdgeStore.getState().getBackendLinks();
|
||||
|
||||
if (graph && graph.id) {
|
||||
const data: Graph = {
|
||||
id: graph.id,
|
||||
name:
|
||||
values?.name || graph.name || `New Agent ${new Date().toISOString()}`,
|
||||
description: values?.description ?? graph.description ?? "",
|
||||
nodes: graphNodes,
|
||||
links: graphLinks,
|
||||
};
|
||||
if (graphsEquivalent(graph, data)) {
|
||||
if (showToast) {
|
||||
toast({
|
||||
title: "No changes to save",
|
||||
description: "The graph is the same as the saved version.",
|
||||
variant: "default",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
await updateGraph({ graphId: graph.id, data: data });
|
||||
} else {
|
||||
const data: Graph = {
|
||||
name: values?.name || `New Agent ${new Date().toISOString()}`,
|
||||
description: values?.description || "",
|
||||
nodes: graphNodes,
|
||||
links: graphLinks,
|
||||
};
|
||||
await createNewGraph({ data: { graph: data } });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Ctrl+S / Cmd+S keyboard shortcut
|
||||
useEffect(() => {
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
@@ -170,7 +127,7 @@ export const useNewSaveControl = ({
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onSubmit]);
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graph) {
|
||||
@@ -181,6 +138,38 @@ export const useNewSaveControl = ({
|
||||
}
|
||||
}, [graph, form]);
|
||||
|
||||
const onSubmit = async (values: SaveableGraphFormValues) => {
|
||||
const graphNodes = useNodeStore.getState().getBackendNodes();
|
||||
const graphLinks = useEdgeStore.getState().getBackendLinks();
|
||||
|
||||
if (graph && graph.id) {
|
||||
const data: Graph = {
|
||||
id: graph.id,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
nodes: graphNodes,
|
||||
links: graphLinks,
|
||||
};
|
||||
if (graphsEquivalent(graph, data)) {
|
||||
toast({
|
||||
title: "No changes to save",
|
||||
description: "The graph is the same as the saved version.",
|
||||
variant: "default",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await updateGraph({ graphId: graph.id, data: data });
|
||||
} else {
|
||||
const data: Graph = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
nodes: graphNodes,
|
||||
links: graphLinks,
|
||||
};
|
||||
await createNewGraph({ data: { graph: data } });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading: isCreating || isUpdating,
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import {
|
||||
beautifyString,
|
||||
cn,
|
||||
fillObjectDefaultsFromSchema,
|
||||
getValue,
|
||||
hasNonNullNonObjectValue,
|
||||
isObject,
|
||||
@@ -159,6 +158,37 @@ export const CustomNode = React.memo(
|
||||
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
||||
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
|
||||
|
||||
const fillDefaults = useCallback((obj: any, schema: any) => {
|
||||
// Iterate over the schema properties
|
||||
for (const key in schema.properties) {
|
||||
if (schema.properties.hasOwnProperty(key)) {
|
||||
const propertySchema = schema.properties[key];
|
||||
|
||||
// If the property is not in the object, initialize it with the default value
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
if (propertySchema.default !== undefined) {
|
||||
obj[key] = propertySchema.default;
|
||||
} else if (propertySchema.type === "object") {
|
||||
// Recursively fill defaults for nested objects
|
||||
obj[key] = fillDefaults({}, propertySchema);
|
||||
} else if (propertySchema.type === "array") {
|
||||
// Recursively fill defaults for arrays
|
||||
obj[key] = fillDefaults([], propertySchema);
|
||||
}
|
||||
} else {
|
||||
// If the property exists, recursively fill defaults for nested objects/arrays
|
||||
if (propertySchema.type === "object") {
|
||||
obj[key] = fillDefaults(obj[key], propertySchema);
|
||||
} else if (propertySchema.type === "array") {
|
||||
obj[key] = fillDefaults(obj[key], propertySchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, []);
|
||||
|
||||
const setHardcodedValues = useCallback(
|
||||
(values: any) => {
|
||||
updateNodeData(id, { hardcodedValues: values });
|
||||
@@ -201,19 +231,17 @@ export const CustomNode = React.memo(
|
||||
|
||||
useEffect(() => {
|
||||
isInitialSetup.current = false;
|
||||
if (data.backend_id) return; // don't auto-modify existing nodes
|
||||
|
||||
if (data.uiType === BlockUIType.AGENT) {
|
||||
setHardcodedValues({
|
||||
...data.hardcodedValues,
|
||||
inputs: fillObjectDefaultsFromSchema(
|
||||
inputs: fillDefaults(
|
||||
data.hardcodedValues.inputs ?? {},
|
||||
data.inputSchema,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
setHardcodedValues(
|
||||
fillObjectDefaultsFromSchema(data.hardcodedValues, data.inputSchema),
|
||||
fillDefaults(data.hardcodedValues, data.inputSchema),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
@@ -829,9 +857,7 @@ export const CustomNode = React.memo(
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
#{(data.backend_id || id).split("-")[0]}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">#{id.split("-")[0]}</span>
|
||||
|
||||
<div className="w-auto grow" />
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import RunnerUIWrapper, { RunnerUIWrapperRef } from "../RunnerUIWrapper";
|
||||
import OttoChatWidget from "@/app/(platform)/build/components/legacy-builder/OttoChatWidget";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useCopyPaste } from "../useCopyPaste";
|
||||
import { useCopyPaste } from "../../../../../../hooks/useCopyPaste";
|
||||
import NewControlPanel from "@/app/(platform)/build/components/NewControlPanel/NewControlPanel";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { BuildActionBar } from "../BuildActionBar";
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface GraphStore {
|
||||
isGraphRunning: boolean;
|
||||
setIsGraphRunning: (isGraphRunning: boolean) => void;
|
||||
}
|
||||
|
||||
export const useGraphStore = create<GraphStore>((set) => ({
|
||||
isGraphRunning: false,
|
||||
setIsGraphRunning: (isGraphRunning: boolean) => set({ isGraphRunning }),
|
||||
}));
|
||||
@@ -4,8 +4,6 @@ import { CustomNode } from "../components/FlowEditor/nodes/CustomNode/CustomNode
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { convertBlockInfoIntoCustomNodeData } from "../components/helper";
|
||||
import { Node } from "@/app/api/__generated__/models/node";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
|
||||
type NodeStore = {
|
||||
nodes: CustomNode[];
|
||||
@@ -24,15 +22,6 @@ type NodeStore = {
|
||||
getHardCodedValues: (nodeId: string) => Record<string, any>;
|
||||
convertCustomNodeToBackendNode: (node: CustomNode) => Node;
|
||||
getBackendNodes: () => Node[];
|
||||
|
||||
updateNodeStatus: (nodeId: string, status: AgentExecutionStatus) => void;
|
||||
getNodeStatus: (nodeId: string) => AgentExecutionStatus | undefined;
|
||||
|
||||
updateNodeExecutionResult: (
|
||||
nodeId: string,
|
||||
result: NodeExecutionResult,
|
||||
) => void;
|
||||
getNodeExecutionResult: (nodeId: string) => NodeExecutionResult | undefined;
|
||||
};
|
||||
|
||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
@@ -114,27 +103,4 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
get().convertCustomNodeToBackendNode(node),
|
||||
);
|
||||
},
|
||||
updateNodeStatus: (nodeId: string, status: AgentExecutionStatus) => {
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, status } } : n,
|
||||
),
|
||||
}));
|
||||
},
|
||||
getNodeStatus: (nodeId: string) => {
|
||||
return get().nodes.find((n) => n.id === nodeId)?.data?.status;
|
||||
},
|
||||
|
||||
updateNodeExecutionResult: (nodeId: string, result: NodeExecutionResult) => {
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId
|
||||
? { ...n, data: { ...n.data, nodeExecutionResult: result } }
|
||||
: n,
|
||||
),
|
||||
}));
|
||||
},
|
||||
getNodeExecutionResult: (nodeId: string) => {
|
||||
return get().nodes.find((n) => n.id === nodeId)?.data?.nodeExecutionResult;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -105,7 +105,6 @@ export const CredentialsInput: FC<{
|
||||
onSelectCredentials: (newValue?: CredentialsMetaInput) => void;
|
||||
siblingInputs?: Record<string, any>;
|
||||
hideIfSingleCredentialAvailable?: boolean;
|
||||
onLoaded?: (loaded: boolean) => void;
|
||||
}> = ({
|
||||
schema,
|
||||
className,
|
||||
@@ -113,7 +112,6 @@ export const CredentialsInput: FC<{
|
||||
onSelectCredentials,
|
||||
siblingInputs,
|
||||
hideIfSingleCredentialAvailable = true,
|
||||
onLoaded,
|
||||
}) => {
|
||||
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
||||
useState(false);
|
||||
@@ -131,13 +129,6 @@ export const CredentialsInput: FC<{
|
||||
const api = useBackendAPI();
|
||||
const credentials = useCredentials(schema, siblingInputs);
|
||||
|
||||
// Report loaded state to parent
|
||||
useEffect(() => {
|
||||
if (onLoaded) {
|
||||
onLoaded(Boolean(credentials && credentials.isLoading === false));
|
||||
}
|
||||
}, [credentials, onLoaded]);
|
||||
|
||||
// Deselect credentials if they do not exist (e.g. provider was changed)
|
||||
useEffect(() => {
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { getV2ListLibraryAgentsResponse } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
|
||||
export function filterAgents(agents: LibraryAgent[], term?: string | null) {
|
||||
const t = term?.trim().toLowerCase();
|
||||
if (!t) return agents;
|
||||
return agents.filter(
|
||||
(a) =>
|
||||
a.name.toLowerCase().includes(t) ||
|
||||
a.description.toLowerCase().includes(t),
|
||||
);
|
||||
}
|
||||
|
||||
export function getInitialData(
|
||||
cachedAgents: LibraryAgent[],
|
||||
searchTerm: string | null,
|
||||
pageSize: number,
|
||||
) {
|
||||
const filtered = filterAgents(
|
||||
cachedAgents as unknown as LibraryAgent[],
|
||||
searchTerm,
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstPageAgents: LibraryAgent[] = filtered.slice(0, pageSize);
|
||||
const totalItems = filtered.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
|
||||
|
||||
const firstPage: getV2ListLibraryAgentsResponse = {
|
||||
status: 200,
|
||||
data: {
|
||||
agents: firstPageAgents,
|
||||
pagination: {
|
||||
total_items: totalItems,
|
||||
total_pages: totalPages,
|
||||
current_page: 1,
|
||||
page_size: pageSize,
|
||||
},
|
||||
} satisfies LibraryAgentResponse,
|
||||
headers: new Headers(),
|
||||
};
|
||||
|
||||
return { pageParams: [1], pages: [firstPage] };
|
||||
}
|
||||
@@ -3,13 +3,9 @@
|
||||
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
|
||||
import { useLibraryPageContext } from "../state-provider";
|
||||
import { useLibraryAgentsStore } from "@/hooks/useLibraryAgents/store";
|
||||
import { getInitialData } from "./helpers";
|
||||
|
||||
export const useLibraryAgentList = () => {
|
||||
const { searchTerm, librarySort } = useLibraryPageContext();
|
||||
const { agents: cachedAgents } = useLibraryAgentsStore();
|
||||
|
||||
const {
|
||||
data: agents,
|
||||
fetchNextPage,
|
||||
@@ -25,7 +21,6 @@ export const useLibraryAgentList = () => {
|
||||
},
|
||||
{
|
||||
query: {
|
||||
initialData: getInitialData(cachedAgents, searchTerm, 8),
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
|
||||
const isMore =
|
||||
|
||||
@@ -14,12 +14,12 @@ import { environment } from "@/services/environment";
|
||||
|
||||
export default function LoginPage() {
|
||||
const {
|
||||
user,
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
isCloudEnv,
|
||||
isUserLoading,
|
||||
isGoogleLoading,
|
||||
@@ -30,7 +30,7 @@ export default function LoginPage() {
|
||||
handleCloseNotAllowedModal,
|
||||
} = useLoginPage();
|
||||
|
||||
if (isUserLoading || user) {
|
||||
if (isUserLoading || isLoggedIn) {
|
||||
return <LoadingLogin />;
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export function useLoginPage() {
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
user,
|
||||
isLoggedIn: !!user,
|
||||
isLoading,
|
||||
isCloudEnv,
|
||||
isUserLoading,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
getGetV2ListLibraryAgentsQueryKey,
|
||||
usePostV2AddMarketplaceAgent,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { usePostV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
@@ -9,7 +6,6 @@ import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/sto
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface UseAgentInfoProps {
|
||||
storeListingVersionId: string;
|
||||
@@ -19,7 +15,6 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { completeStep } = useOnboarding();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
mutateAsync: addMarketplaceAgentToLibrary,
|
||||
@@ -51,10 +46,6 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
if (isAddingAgentFirstTime) {
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
analytics.sendDatafastEvent("add_to_library", {
|
||||
name: data.name,
|
||||
id: data.id,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/app/api/__generated__/endpoints/auth/auth";
|
||||
import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useTimezoneDetection } from "@/app/(platform)/profile/(user)/settings/useTimezoneDetection";
|
||||
import { useTimezoneDetection } from "@/hooks/useTimezoneDetection";
|
||||
import * as React from "react";
|
||||
import SettingsLoading from "./loading";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -28,7 +28,6 @@ export default function SettingsPage() {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useTimezoneDetection(timezone);
|
||||
|
||||
const { user, isUserLoading } = useSupabase();
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
@layer components {
|
||||
.agpt-border-input {
|
||||
@apply m-0.5 border border-input data-[state=open]:border-gray-400 data-[state=open]:ring-1 data-[state=open]:ring-gray-400 focus:border-gray-400 focus:outline-none focus:ring-1 focus:ring-gray-400;
|
||||
@apply m-0.5 border border-input focus:border-gray-400 focus:outline-none focus:ring-1 focus:ring-gray-400 data-[state=open]:border-gray-400 data-[state=open]:ring-1 data-[state=open]:ring-gray-400;
|
||||
}
|
||||
|
||||
.agpt-shadow-input {
|
||||
|
||||
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 border-neutral-900 shadow data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-50 dark:border-neutral-800 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900 dark:focus-visible:ring-neutral-300",
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 border-neutral-900 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-50 dark:border-neutral-800 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none dark:ring-offset-neutral-950 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400 dark:focus:ring-neutral-300">
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -31,7 +31,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-neutral-100 focus:bg-neutral-100 dark:data-[state=open]:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-neutral-100 data-[state=open]:bg-neutral-100 dark:focus:bg-neutral-800 dark:data-[state=open]:bg-neutral-800",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
@@ -88,7 +88,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
@@ -104,7 +104,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -128,7 +128,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -24,7 +24,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"agpt-border-input agpt-shadow-input flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 data-[state=open]:border-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-neutral-400 [&>span]:line-clamp-1",
|
||||
"agpt-border-input agpt-shadow-input flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=open]:border-gray-400 dark:placeholder:text-neutral-400 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -123,7 +123,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -64,7 +64,7 @@ const SheetContent = React.forwardRef<
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity data-[state=open]:bg-neutral-100 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none dark:ring-offset-neutral-950 dark:data-[state=open]:bg-neutral-800 dark:focus:ring-neutral-300">
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
|
||||
@@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors data-[state=selected]:bg-neutral-100 hover:bg-neutral-100/50 dark:data-[state=selected]:bg-neutral-800 dark:hover:bg-neutral-800/50",
|
||||
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function EmailNotAllowedModal({ isOpen, onClose }: Props) {
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="py-4">
|
||||
<WaitlistErrorContent />
|
||||
<WaitlistErrorContent onClose={onClose} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
import { Button } from "../atoms/Button/Button";
|
||||
import { Text } from "../atoms/Text/Text";
|
||||
|
||||
interface Props {
|
||||
onBackToLogin?: () => void;
|
||||
interface WaitlistErrorContentProps {
|
||||
onClose: () => void;
|
||||
closeButtonText?: string;
|
||||
closeButtonVariant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
export function WaitlistErrorContent(props: Props) {
|
||||
export function WaitlistErrorContent({
|
||||
onClose,
|
||||
closeButtonText = "Close",
|
||||
closeButtonVariant = "primary",
|
||||
}: WaitlistErrorContentProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<Text variant="h3">We're in closed beta</Text>
|
||||
<Text variant="h3">Join the Waitlist</Text>
|
||||
<div className="flex flex-col gap-4 text-center">
|
||||
<Text variant="large" className="text-center">
|
||||
Looks like your email isn't in our early access list just yet.
|
||||
Join the waitlist and we will let you know the moment we open up
|
||||
access!
|
||||
<Text variant="large-medium" className="text-center">
|
||||
The AutoGPT Platform is currently in closed beta. Your email address
|
||||
isn't on our current allowlist for early access.
|
||||
</Text>
|
||||
<Text variant="body" className="text-center">
|
||||
Join our waitlist to get notified when we open up access!
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
window.open("https://agpt.co/waitlist", "_blank");
|
||||
}}
|
||||
>
|
||||
Join Waitlist
|
||||
</Button>
|
||||
{props.onBackToLogin ? (
|
||||
<Button variant="secondary" onClick={props.onBackToLogin}>
|
||||
Back to Login
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant={closeButtonVariant} onClick={onClose}>
|
||||
{closeButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text variant="small" className="text-center text-muted-foreground">
|
||||
Already joined? Double-check you are using the same email you signed
|
||||
up with. Need a hand? Emails us at{" "}
|
||||
Already signed up for the waitlist? Make sure you're using the
|
||||
exact same email address you used when signing up.
|
||||
</Text>
|
||||
<Text variant="small" className="text-center text-muted-foreground">
|
||||
If you're not sure which email you used or need help, contact us
|
||||
at{" "}
|
||||
<a
|
||||
href="mailto:contact@agpt.co"
|
||||
className="underline hover:text-foreground"
|
||||
@@ -47,7 +58,7 @@ export function WaitlistErrorContent(props: Props) {
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground"
|
||||
>
|
||||
message us on Discord
|
||||
reach out on Discord
|
||||
</a>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { useGetV1ListAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
|
||||
|
||||
import BackendAPI from "@/lib/autogpt-server-api/client";
|
||||
import type { GraphExecution, GraphID } from "@/lib/autogpt-server-api/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
NotificationState,
|
||||
categorizeExecutions,
|
||||
handleExecutionUpdate,
|
||||
} from "./helpers";
|
||||
import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents";
|
||||
|
||||
type AgentInfoMap = Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>;
|
||||
|
||||
export function useAgentActivityDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [api] = useState(() => new BackendAPI());
|
||||
const { agentInfoMap } = useLibraryAgents();
|
||||
|
||||
const [notifications, setNotifications] = useState<NotificationState>({
|
||||
activeExecutions: [],
|
||||
@@ -24,6 +30,13 @@ export function useAgentActivityDropdown() {
|
||||
});
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
|
||||
|
||||
const {
|
||||
data: agents,
|
||||
isSuccess: agentsSuccess,
|
||||
error: agentsError,
|
||||
} = useGetV2ListLibraryAgents();
|
||||
|
||||
const {
|
||||
data: executions,
|
||||
@@ -33,6 +46,59 @@ export function useAgentActivityDropdown() {
|
||||
query: { select: (res) => (res.status === 200 ? res.data : null) },
|
||||
});
|
||||
|
||||
// Create a map of library agents
|
||||
useEffect(() => {
|
||||
if (agentsError) {
|
||||
Sentry.captureException(agentsError, {
|
||||
tags: {
|
||||
context: "library_agents_fetch",
|
||||
},
|
||||
});
|
||||
toast.error("Failed to load agent information", {
|
||||
description:
|
||||
"There was a problem connecting to our servers. Agent activity may be limited.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (agents && agentsSuccess) {
|
||||
if (agents.status !== 200) {
|
||||
Sentry.captureException(new Error("Failed to load library agents"), {
|
||||
extra: {
|
||||
status: agents.status,
|
||||
error: agents.data,
|
||||
},
|
||||
});
|
||||
toast.error("Invalid agent data received", {
|
||||
description:
|
||||
"The server returned invalid data. Agent activity may be limited.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const libraryAgents = agents.data;
|
||||
|
||||
if (!libraryAgents.agents || !libraryAgents.agents.length) return;
|
||||
|
||||
const agentMap = new Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>();
|
||||
|
||||
libraryAgents.agents.forEach((agent) => {
|
||||
if (agent.graph_id && agent.id) {
|
||||
agentMap.set(agent.graph_id, {
|
||||
name: agent.name || `Agent ${agent.graph_id.slice(0, 8)}`,
|
||||
description: agent.description || "",
|
||||
library_agent_id: agent.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setAgentInfoMap(agentMap);
|
||||
}
|
||||
}, [agents, agentsSuccess, agentsError]);
|
||||
|
||||
// Handle real-time execution updates
|
||||
const handleExecutionEvent = useCallback(
|
||||
(execution: GraphExecution) => {
|
||||
@@ -90,8 +156,8 @@ export function useAgentActivityDropdown() {
|
||||
return {
|
||||
...notifications,
|
||||
isConnected,
|
||||
isReady: executionsSuccess,
|
||||
error: executionsError,
|
||||
isReady: executionsSuccess && agentsSuccess,
|
||||
error: executionsError || agentsError,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
};
|
||||
|
||||
@@ -45,8 +45,8 @@ export const NavbarView = ({ isLoggedIn }: NavbarViewProps) => {
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
{isLoggedIn ? (
|
||||
<div className="hidden flex-1 items-center justify-end gap-4 md:flex">
|
||||
<div className="hidden flex-1 items-center justify-end gap-4 md:flex">
|
||||
{isLoggedIn ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<AgentActivityDropdown />
|
||||
{profile && <Wallet />}
|
||||
@@ -57,13 +57,11 @@ export const NavbarView = ({ isLoggedIn }: NavbarViewProps) => {
|
||||
menuItemGroups={dynamicMenuItems}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-end">
|
||||
) : (
|
||||
<LoginButton />
|
||||
</div>
|
||||
)}
|
||||
{/* <ThemeToggle /> */}
|
||||
)}
|
||||
{/* <ThemeToggle /> */}
|
||||
</div>
|
||||
</nav>
|
||||
{/* Mobile Navbar - Adjust positioning */}
|
||||
<>
|
||||
|
||||
@@ -25,7 +25,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-neutral-100 focus:bg-neutral-100 dark:data-[state=open]:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-neutral-100 data-[state=open]:bg-neutral-100 dark:focus:bg-neutral-800 dark:data-[state=open]:bg-neutral-800",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
@@ -82,7 +82,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
@@ -98,7 +98,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -122,7 +122,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-neutral-100 focus:text-neutral-900 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -109,7 +109,7 @@ const TabsLineTrigger = React.forwardRef<
|
||||
elementRef.current = node;
|
||||
}}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-3 font-sans text-[1rem] font-medium leading-[1.5rem] text-zinc-700 transition-all data-[state=active]:text-purple-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-3 font-sans text-[1rem] font-medium leading-[1.5rem] text-zinc-700 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-purple-600",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,49 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "../../__legacy__/ui/button";
|
||||
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useTallyPopup } from "./useTallyPopup";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { getCurrentUser } from "@/lib/supabase/actions";
|
||||
|
||||
export function TallyPopupSimple() {
|
||||
const { state, handlers } = useTallyPopup();
|
||||
const TallyPopupSimple = () => {
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [sentryReplayId, setSentryReplayId] = useState("");
|
||||
const [replayUrl, setReplayUrl] = useState("");
|
||||
const [pageUrl, setPageUrl] = useState("");
|
||||
const [userAgent, setUserAgent] = useState("");
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
// const [userId, setUserId] = useState<string>("");
|
||||
const [userEmail, setUserEmail] = useState<string>("");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
if (state.isFormVisible) {
|
||||
const [show_tutorial, setShowTutorial] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTutorial(pathname.includes("build"));
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set client-side values
|
||||
if (typeof window !== "undefined") {
|
||||
setPageUrl(window.location.href);
|
||||
setUserAgent(window.navigator.userAgent);
|
||||
|
||||
const replay = Sentry.getReplay();
|
||||
|
||||
if (replay) {
|
||||
const replayId = replay.getReplayId();
|
||||
|
||||
if (replayId) {
|
||||
setSentryReplayId(replayId);
|
||||
const orgSlug = "significant-gravitas";
|
||||
setReplayUrl(`https://${orgSlug}.sentry.io/replays/${replayId}/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check authentication status using server action (works with httpOnly cookies)
|
||||
getCurrentUser().then(({ user }) => {
|
||||
setIsAuthenticated(user != null);
|
||||
// setUserId(user?.id || "");
|
||||
setUserEmail(user?.email || "");
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load Tally script
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://tally.so/widgets/embed.js";
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Setup event listeners for Tally events
|
||||
const handleTallyMessage = (event: MessageEvent) => {
|
||||
if (typeof event.data === "string") {
|
||||
// Ignore iframe-resizer messages
|
||||
if (
|
||||
event.data.startsWith("[iFrameSize") ||
|
||||
event.data.startsWith("[iFrameResizer")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Only process Tally events
|
||||
if (!data.event?.startsWith("Tally.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.event === "Tally.FormLoaded") {
|
||||
setIsFormVisible(true);
|
||||
|
||||
// Flush Sentry replay when form opens
|
||||
if (typeof window !== "undefined") {
|
||||
const replay = Sentry.getReplay();
|
||||
if (replay) {
|
||||
replay.flush();
|
||||
}
|
||||
}
|
||||
} else if (data.event === "Tally.PopupClosed") {
|
||||
setIsFormVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Only log errors for messages we care about
|
||||
if (event.data.includes("Tally")) {
|
||||
console.error("Error parsing Tally message:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleTallyMessage);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
window.removeEventListener("message", handleTallyMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isFormVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resetTutorial = () => {
|
||||
router.push("/build?resetTutorial=true");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 right-24 z-20 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
|
||||
{state.showTutorial && (
|
||||
{show_tutorial && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handlers.handleResetTutorial}
|
||||
variant="default"
|
||||
onClick={resetTutorial}
|
||||
className="mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left font-sans text-lg font-medium leading-6"
|
||||
>
|
||||
Tutorial
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
className="h-14 w-14 rounded-full bg-[rgba(65,65,64,1)]"
|
||||
variant="default"
|
||||
data-tally-open="3yx2L0"
|
||||
data-tally-emoji-text="👋"
|
||||
data-tally-emoji-animation="wave"
|
||||
data-sentry-replay-id={state.sentryReplayId || "not-initialized"}
|
||||
data-sentry-replay-url={state.replayUrl || "not-initialized"}
|
||||
data-user-agent={state.userAgent}
|
||||
data-page-url={state.pageUrl}
|
||||
data-sentry-replay-id={sentryReplayId || "not-initialized"}
|
||||
data-sentry-replay-url={replayUrl || "not-initialized"}
|
||||
data-user-agent={userAgent}
|
||||
data-page-url={pageUrl}
|
||||
data-is-authenticated={
|
||||
state.isAuthenticated === null
|
||||
? "unknown"
|
||||
: String(state.isAuthenticated)
|
||||
isAuthenticated === null ? "unknown" : String(isAuthenticated)
|
||||
}
|
||||
data-email={state.userEmail || "not-authenticated"}
|
||||
data-email={userEmail || "not-authenticated"}
|
||||
// data-user-id={userId || "not-authenticated"}
|
||||
>
|
||||
<QuestionMarkCircledIcon className="h-6 w-6" />
|
||||
<span className="">Give Feedback</span>
|
||||
<QuestionMarkCircledIcon className="h-14 w-14" />
|
||||
<span className="sr-only">Reach Out</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TallyPopupSimple;
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { getCurrentUser } from "@/lib/supabase/actions";
|
||||
|
||||
export function useTallyPopup() {
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [sentryReplayId, setSentryReplayId] = useState("");
|
||||
const [replayUrl, setReplayUrl] = useState("");
|
||||
const [pageUrl, setPageUrl] = useState("");
|
||||
const [userAgent, setUserAgent] = useState("");
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const [userEmail, setUserEmail] = useState<string>("");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [showTutorial, setShowTutorial] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTutorial(pathname.includes("build"));
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set client-side values
|
||||
if (typeof window !== "undefined") {
|
||||
setPageUrl(window.location.href);
|
||||
setUserAgent(window.navigator.userAgent);
|
||||
|
||||
const replay = Sentry.getReplay();
|
||||
|
||||
if (replay) {
|
||||
const replayId = replay.getReplayId();
|
||||
|
||||
if (replayId) {
|
||||
setSentryReplayId(replayId);
|
||||
const orgSlug = "significant-gravitas";
|
||||
setReplayUrl(`https://${orgSlug}.sentry.io/replays/${replayId}/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check authentication status using server action (works with httpOnly cookies)
|
||||
getCurrentUser().then(({ user }) => {
|
||||
setIsAuthenticated(user != null);
|
||||
setUserEmail(user?.email || "");
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load Tally script
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://tally.so/widgets/embed.js";
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Setup event listeners for Tally events
|
||||
const handleTallyMessage = (event: MessageEvent) => {
|
||||
if (typeof event.data === "string") {
|
||||
// Ignore iframe-resizer messages
|
||||
if (
|
||||
event.data.startsWith("[iFrameSize") ||
|
||||
event.data.startsWith("[iFrameResizer")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Only process Tally events
|
||||
if (!data.event?.startsWith("Tally.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.event === "Tally.FormLoaded") {
|
||||
setIsFormVisible(true);
|
||||
|
||||
// Flush Sentry replay when form opens
|
||||
if (typeof window !== "undefined") {
|
||||
const replay = Sentry.getReplay();
|
||||
if (replay) {
|
||||
replay.flush();
|
||||
}
|
||||
}
|
||||
} else if (data.event === "Tally.PopupClosed") {
|
||||
setIsFormVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Only log errors for messages we care about
|
||||
if (event.data.includes("Tally")) {
|
||||
console.error("Error parsing Tally message:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleTallyMessage);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
window.removeEventListener("message", handleTallyMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function handleResetTutorial() {
|
||||
router.push("/build?resetTutorial=true");
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
showTutorial,
|
||||
sentryReplayId,
|
||||
replayUrl,
|
||||
pageUrl,
|
||||
userAgent,
|
||||
isAuthenticated,
|
||||
isFormVisible,
|
||||
userEmail,
|
||||
},
|
||||
handlers: {
|
||||
handleResetTutorial,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -21,15 +21,17 @@ import {
|
||||
Node,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { deepEquals, getTypeColor, pruneEmptyValues } from "@/lib/utils";
|
||||
import {
|
||||
deepEquals,
|
||||
getTypeColor,
|
||||
removeEmptyStringsAndNulls,
|
||||
} from "@/lib/utils";
|
||||
import { MarkerType } from "@xyflow/react";
|
||||
import { default as NextLink } from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
|
||||
|
||||
export default function useAgentGraph(
|
||||
flowID?: GraphID,
|
||||
@@ -42,7 +44,6 @@ export default function useAgentGraph(
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const api = useBackendAPI();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [isScheduling, setIsScheduling] = useState(false);
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
@@ -597,10 +598,7 @@ export default function useAgentGraph(
|
||||
return {};
|
||||
}
|
||||
|
||||
return rebuildObjectUsingSchema(
|
||||
blockSchema,
|
||||
pruneEmptyValues(node.data.hardcodedValues),
|
||||
);
|
||||
return rebuildObjectUsingSchema(blockSchema, node.data.hardcodedValues);
|
||||
},
|
||||
[availableBlocks],
|
||||
);
|
||||
@@ -609,7 +607,6 @@ export default function useAgentGraph(
|
||||
const links = xyEdges.map((edge): LinkCreatable => {
|
||||
let sourceName = edge.sourceHandle || "";
|
||||
const sourceNode = xyNodes.find((node) => node.id === edge.source);
|
||||
const sinkNode = xyNodes.find((node) => node.id === edge.target);
|
||||
|
||||
// Special case for SmartDecisionMakerBlock
|
||||
if (
|
||||
@@ -619,8 +616,8 @@ export default function useAgentGraph(
|
||||
sourceName = `tools_^_${normalizeToolName(getToolFuncName(edge.target))}_~_${normalizeToolName(edge.targetHandle || "")}`;
|
||||
}
|
||||
return {
|
||||
source_id: sourceNode?.data.backend_id ?? edge.source,
|
||||
sink_id: sinkNode?.data.backend_id ?? edge.target,
|
||||
source_id: edge.source,
|
||||
sink_id: edge.target,
|
||||
source_name: sourceName,
|
||||
sink_name: edge.targetHandle || "",
|
||||
};
|
||||
@@ -632,7 +629,7 @@ export default function useAgentGraph(
|
||||
recommended_schedule_cron: agentRecommendedScheduleCron || null,
|
||||
nodes: xyNodes.map(
|
||||
(node): NodeCreatable => ({
|
||||
id: node.data.backend_id ?? node.id,
|
||||
id: node.id,
|
||||
block_id: node.data.block_id,
|
||||
input_default: prepareNodeInputData(node),
|
||||
metadata: {
|
||||
@@ -698,9 +695,8 @@ export default function useAgentGraph(
|
||||
|
||||
// Update the node IDs on the frontend
|
||||
setSavedAgent(newSavedAgent);
|
||||
|
||||
setXYNodes((prev) =>
|
||||
newSavedAgent.nodes
|
||||
setXYNodes((prev) => {
|
||||
return newSavedAgent.nodes
|
||||
.map((backendNode) => {
|
||||
const key = `${backendNode.block_id}_${backendNode.metadata.position.x}_${backendNode.metadata.position.y}`;
|
||||
const frontendNodeID = blockIDToNodeIDMap[key];
|
||||
@@ -713,26 +709,22 @@ export default function useAgentGraph(
|
||||
position,
|
||||
data: {
|
||||
...frontendNode.data,
|
||||
// NOTE: we don't update `node.id` because it would also require
|
||||
// updating many references in other places. Instead, we keep the
|
||||
// backend node ID in `node.data.backend_id`.
|
||||
backend_id: backendNode.id,
|
||||
metadata,
|
||||
|
||||
// Reset & close node output
|
||||
isOutputOpen: false,
|
||||
hardcodedValues: removeEmptyStringsAndNulls(
|
||||
frontendNode.data.hardcodedValues,
|
||||
),
|
||||
status: undefined,
|
||||
executionResults: undefined,
|
||||
backend_id: backendNode.id,
|
||||
executionResults: [],
|
||||
metadata,
|
||||
},
|
||||
} satisfies CustomNode)
|
||||
: _backendNodeToXYNode(backendNode, newSavedAgent); // fallback
|
||||
})
|
||||
.filter((node) => node !== null),
|
||||
);
|
||||
|
||||
.filter((node) => node !== null);
|
||||
});
|
||||
// Reset bead count
|
||||
setXYEdges((edges) =>
|
||||
edges.map(
|
||||
setXYEdges((edges) => {
|
||||
return edges.map(
|
||||
(edge): CustomEdge => ({
|
||||
...edge,
|
||||
data: {
|
||||
@@ -743,8 +735,8 @@ export default function useAgentGraph(
|
||||
beadData: new Map(),
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
return newSavedAgent;
|
||||
}, [
|
||||
api,
|
||||
@@ -763,11 +755,6 @@ export default function useAgentGraph(
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await _saveAgent();
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
completeStep("BUILDER_SAVE_AGENT");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { storage, Key } from "@/services/storage/local-storage";
|
||||
import {
|
||||
getV2ListLibraryAgents,
|
||||
type getV2ListLibraryAgentsResponse,
|
||||
} from "@/app/api/__generated__/endpoints/library/library";
|
||||
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
|
||||
export type AgentInfo = LibraryAgent;
|
||||
|
||||
type AgentStore = {
|
||||
agents: AgentInfo[];
|
||||
lastUpdatedAt?: number;
|
||||
isRefreshing: boolean;
|
||||
error?: unknown;
|
||||
loadFromCache: () => void;
|
||||
refreshAll: () => Promise<void>;
|
||||
};
|
||||
|
||||
type CachedAgents = {
|
||||
agents: LibraryAgent[];
|
||||
lastUpdatedAt: number;
|
||||
};
|
||||
|
||||
async function fetchAllLibraryAgents() {
|
||||
const pageSize = 100;
|
||||
let page = 1;
|
||||
const all: LibraryAgent[] = [];
|
||||
|
||||
let res: getV2ListLibraryAgentsResponse | undefined;
|
||||
try {
|
||||
res = await getV2ListLibraryAgents({ page, page_size: pageSize });
|
||||
} catch (err) {
|
||||
Sentry.captureException(err, { tags: { context: "library_agents_fetch" } });
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!res || res.status !== 200) return all;
|
||||
|
||||
const { agents, pagination } = res.data;
|
||||
all.push(...agents);
|
||||
|
||||
const totalPages = pagination?.total_pages ?? 1;
|
||||
|
||||
for (page = 2; page <= totalPages; page += 1) {
|
||||
try {
|
||||
const next = await getV2ListLibraryAgents({ page, page_size: pageSize });
|
||||
if (next.status === 200) {
|
||||
all.push(...next.data.agents);
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.captureException(err, {
|
||||
tags: { context: "library_agents_fetch" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
function persistCache(cached: CachedAgents) {
|
||||
try {
|
||||
storage.set(Key.LIBRARY_AGENTS_CACHE, JSON.stringify(cached));
|
||||
} catch (error) {
|
||||
// Ignore cache failures
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to persist library agents cache", error);
|
||||
Sentry.captureException(error, {
|
||||
tags: { context: "library_agents_cache_persist" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function readCache(): CachedAgents | undefined {
|
||||
try {
|
||||
const raw = storage.get(Key.LIBRARY_AGENTS_CACHE);
|
||||
if (!raw) return;
|
||||
return JSON.parse(raw) as CachedAgents;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const useLibraryAgentsStore = create<AgentStore>((set, get) => ({
|
||||
agents: [],
|
||||
lastUpdatedAt: undefined,
|
||||
isRefreshing: false,
|
||||
error: undefined,
|
||||
loadFromCache: () => {
|
||||
const cached = readCache();
|
||||
if (cached?.agents?.length) {
|
||||
set({ agents: cached.agents, lastUpdatedAt: cached.lastUpdatedAt });
|
||||
}
|
||||
},
|
||||
refreshAll: async () => {
|
||||
if (get().isRefreshing) return;
|
||||
set({ isRefreshing: true, error: undefined });
|
||||
try {
|
||||
const agents = await fetchAllLibraryAgents();
|
||||
const snapshot: CachedAgents = { agents, lastUpdatedAt: Date.now() };
|
||||
persistCache(snapshot);
|
||||
set({ agents, lastUpdatedAt: snapshot.lastUpdatedAt });
|
||||
} catch (error) {
|
||||
set({ error });
|
||||
} finally {
|
||||
set({ isRefreshing: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export function buildAgentInfoMap(agents: AgentInfo[]) {
|
||||
const map = new Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>();
|
||||
agents.forEach((a) => {
|
||||
if (a.graph_id && a.id) {
|
||||
map.set(a.graph_id, {
|
||||
name:
|
||||
a.name || (a.graph_id ? `Agent ${a.graph_id.slice(0, 8)}` : "Agent"),
|
||||
description: a.description || "",
|
||||
library_agent_id: a.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { buildAgentInfoMap, useLibraryAgentsStore } from "./store";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function useLibraryAgents() {
|
||||
const { agents, isRefreshing, lastUpdatedAt, loadFromCache, refreshAll } =
|
||||
useLibraryAgentsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
loadFromCache();
|
||||
void refreshAll();
|
||||
initialized = true;
|
||||
}
|
||||
}, [loadFromCache, refreshAll]);
|
||||
|
||||
const agentInfoMap = useMemo(() => buildAgentInfoMap(agents), [agents]);
|
||||
|
||||
return { agents, agentInfoMap, isRefreshing, lastUpdatedAt };
|
||||
}
|
||||
@@ -2,11 +2,7 @@ import { type ClassValue, clsx } from "clsx";
|
||||
import { isEmpty as _isEmpty } from "lodash";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
BlockIOObjectSubSchema,
|
||||
BlockIORootSchema,
|
||||
Category,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { Category } from "@/lib/autogpt-server-api/types";
|
||||
import { NodeDimension } from "@/app/(platform)/build/components/legacy-builder/Flow/Flow";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
@@ -181,73 +177,32 @@ export function setNestedProperty(obj: any, path: string, value: any) {
|
||||
current[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
export function pruneEmptyValues(
|
||||
obj: any,
|
||||
removeEmptyStrings: boolean = true,
|
||||
): any {
|
||||
export function removeEmptyStringsAndNulls(obj: any): any {
|
||||
if (Array.isArray(obj)) {
|
||||
// If obj is an array, recursively check each element,
|
||||
// but element removal is avoided to prevent index changes.
|
||||
return obj.map((item) =>
|
||||
item === undefined || item === null
|
||||
? ""
|
||||
: pruneEmptyValues(item, removeEmptyStrings),
|
||||
: removeEmptyStringsAndNulls(item),
|
||||
);
|
||||
} else if (typeof obj === "object" && obj !== null) {
|
||||
// If obj is an object, recursively remove empty strings and nulls from its properties
|
||||
for (const key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) continue;
|
||||
|
||||
const value = obj[key];
|
||||
if (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
(typeof value === "string" && value === "" && removeEmptyStrings)
|
||||
) {
|
||||
delete obj[key];
|
||||
} else if (typeof value === "object") {
|
||||
obj[key] = pruneEmptyValues(value, removeEmptyStrings);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function fillObjectDefaultsFromSchema(
|
||||
obj: Record<any, any>,
|
||||
schema: BlockIORootSchema | BlockIOObjectSubSchema,
|
||||
) {
|
||||
for (const key in schema.properties) {
|
||||
if (!schema.properties.hasOwnProperty(key)) continue;
|
||||
|
||||
const propertySchema = schema.properties[key];
|
||||
|
||||
if ("default" in propertySchema && propertySchema.default !== undefined) {
|
||||
// Apply simple default values
|
||||
obj[key] ??= propertySchema.default;
|
||||
} else if (
|
||||
propertySchema.type === "object" &&
|
||||
"properties" in propertySchema
|
||||
) {
|
||||
// Recursively fill defaults for nested objects
|
||||
obj[key] = fillObjectDefaultsFromSchema(obj[key] ?? {}, propertySchema);
|
||||
} else if (propertySchema.type === "array") {
|
||||
obj[key] ??= [];
|
||||
// If the array items are objects, fill their defaults as well
|
||||
if (
|
||||
Array.isArray(obj[key]) &&
|
||||
propertySchema.items?.type === "object" &&
|
||||
"properties" in propertySchema.items
|
||||
) {
|
||||
for (const item of obj[key]) {
|
||||
if (typeof item === "object" && item !== null) {
|
||||
fillObjectDefaultsFromSchema(item, propertySchema.items);
|
||||
}
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const value = obj[key];
|
||||
if (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
(typeof value === "string" && value === "")
|
||||
) {
|
||||
delete obj[key];
|
||||
} else {
|
||||
obj[key] = removeEmptyStringsAndNulls(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export enum Key {
|
||||
COPIED_FLOW_DATA = "copied-flow-data",
|
||||
SHEPHERD_TOUR = "shepherd-tour",
|
||||
WALLET_LAST_SEEN_CREDITS = "wallet-last-seen-credits",
|
||||
LIBRARY_AGENTS_CACHE = "library-agents-cache",
|
||||
}
|
||||
|
||||
function get(key: Key) {
|
||||
|
||||
@@ -74,6 +74,46 @@ test.describe("Library", () => {
|
||||
test.expect(allAgents.length).toEqual(displayedCount);
|
||||
});
|
||||
|
||||
test("sorting works correctly", async ({ page }) => {
|
||||
await page.goto("/library");
|
||||
|
||||
const initialAgents = await libraryPage.getAgents();
|
||||
expect(initialAgents.length).toBeGreaterThan(0);
|
||||
|
||||
await libraryPage.selectSortOption(page, "Creation Date");
|
||||
await libraryPage.waitForAgentsToLoad();
|
||||
|
||||
const creationDateSortOption = await libraryPage.getCurrentSortOption();
|
||||
expect(creationDateSortOption).toContain("Creation Date");
|
||||
|
||||
const creationDateAgents = await libraryPage.getAgents();
|
||||
expect(creationDateAgents.length).toBeGreaterThan(0);
|
||||
|
||||
await libraryPage.selectSortOption(page, "Last Modified");
|
||||
await libraryPage.waitForAgentsToLoad();
|
||||
|
||||
const lastModifiedSortOption = await libraryPage.getCurrentSortOption();
|
||||
expect(lastModifiedSortOption).toContain("Last Modified");
|
||||
|
||||
const lastModifiedAgents = await libraryPage.getAgents();
|
||||
expect(lastModifiedAgents.length).toBeGreaterThan(0);
|
||||
|
||||
if (initialAgents.length > 1) {
|
||||
const initialFirstAgentId = initialAgents[0].id;
|
||||
const creationDateFirstAgentId = creationDateAgents[0].id;
|
||||
const lastModifiedFirstAgentId = lastModifiedAgents[0].id;
|
||||
|
||||
expect(
|
||||
creationDateFirstAgentId !== initialFirstAgentId ||
|
||||
lastModifiedFirstAgentId !== initialFirstAgentId ||
|
||||
creationDateFirstAgentId !== lastModifiedFirstAgentId,
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
expect(creationDateAgents.length).toEqual(initialAgents.length);
|
||||
expect(lastModifiedAgents.length).toEqual(initialAgents.length);
|
||||
});
|
||||
|
||||
test("searching works correctly", async ({ page }) => {
|
||||
await page.goto("/library");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user