Merge branch 'swiftyos/vector-search' of github.com:Significant-Gravitas/AutoGPT into swiftyos/vector-search

This commit is contained in:
Swifty
2025-12-18 19:29:17 +01:00
111 changed files with 11600 additions and 1405 deletions

View File

@@ -11,7 +11,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
# operations-per-run: 5000
stale-issue-message: >

View File

@@ -61,6 +61,6 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v6
with:
sync-labels: true

View File

@@ -0,0 +1,108 @@
{
"action": "created",
"discussion": {
"repository_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT",
"category": {
"id": 12345678,
"node_id": "DIC_kwDOJKSTjM4CXXXX",
"repository_id": 614765452,
"emoji": ":pray:",
"name": "Q&A",
"description": "Ask the community for help",
"created_at": "2023-03-16T09:21:07Z",
"updated_at": "2023-03-16T09:21:07Z",
"slug": "q-a",
"is_answerable": true
},
"answer_html_url": null,
"answer_chosen_at": null,
"answer_chosen_by": null,
"html_url": "https://github.com/Significant-Gravitas/AutoGPT/discussions/9999",
"id": 5000000001,
"node_id": "D_kwDOJKSTjM4AYYYY",
"number": 9999,
"title": "How do I configure custom blocks?",
"user": {
"login": "curious-user",
"id": 22222222,
"node_id": "MDQ6VXNlcjIyMjIyMjIy",
"avatar_url": "https://avatars.githubusercontent.com/u/22222222?v=4",
"url": "https://api.github.com/users/curious-user",
"html_url": "https://github.com/curious-user",
"type": "User",
"site_admin": false
},
"state": "open",
"state_reason": null,
"locked": false,
"comments": 0,
"created_at": "2024-12-01T17:00:00Z",
"updated_at": "2024-12-01T17:00:00Z",
"author_association": "NONE",
"active_lock_reason": null,
"body": "## Question\n\nI'm trying to create a custom block for my specific use case. I've read the documentation but I'm not sure how to:\n\n1. Define the input/output schema\n2. Handle authentication\n3. Test my block locally\n\nCan someone point me to examples or provide guidance?\n\n## Environment\n\n- AutoGPT Platform version: latest\n- Python: 3.11",
"reactions": {
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/discussions/9999/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/discussions/9999/timeline"
},
"repository": {
"id": 614765452,
"node_id": "R_kgDOJKSTjA",
"name": "AutoGPT",
"full_name": "Significant-Gravitas/AutoGPT",
"private": false,
"owner": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"url": "https://api.github.com/users/Significant-Gravitas",
"html_url": "https://github.com/Significant-Gravitas",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Significant-Gravitas/AutoGPT",
"description": "AutoGPT is the vision of accessible AI for everyone, to use and to build on.",
"fork": false,
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT",
"created_at": "2023-03-16T09:21:07Z",
"updated_at": "2024-12-01T17:00:00Z",
"pushed_at": "2024-12-01T12:00:00Z",
"stargazers_count": 170000,
"watchers_count": 170000,
"language": "Python",
"has_discussions": true,
"forks_count": 45000,
"visibility": "public",
"default_branch": "master"
},
"organization": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"url": "https://api.github.com/orgs/Significant-Gravitas",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"description": ""
},
"sender": {
"login": "curious-user",
"id": 22222222,
"node_id": "MDQ6VXNlcjIyMjIyMjIy",
"avatar_url": "https://avatars.githubusercontent.com/u/22222222?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/curious-user",
"html_url": "https://github.com/curious-user",
"type": "User",
"site_admin": false
}
}

View File

@@ -0,0 +1,112 @@
{
"action": "opened",
"issue": {
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/issues/12345",
"repository_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT",
"labels_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/issues/12345/labels{/name}",
"comments_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/issues/12345/comments",
"events_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/issues/12345/events",
"html_url": "https://github.com/Significant-Gravitas/AutoGPT/issues/12345",
"id": 2000000001,
"node_id": "I_kwDOJKSTjM5wXXXX",
"number": 12345,
"title": "Bug: Application crashes when processing large files",
"user": {
"login": "bug-reporter",
"id": 11111111,
"node_id": "MDQ6VXNlcjExMTExMTEx",
"avatar_url": "https://avatars.githubusercontent.com/u/11111111?v=4",
"url": "https://api.github.com/users/bug-reporter",
"html_url": "https://github.com/bug-reporter",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 5272676214,
"node_id": "LA_kwDOJKSTjM8AAAABOkandg",
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/labels/bug",
"name": "bug",
"color": "d73a4a",
"default": true,
"description": "Something isn't working"
}
],
"state": "open",
"locked": false,
"assignee": null,
"assignees": [],
"milestone": null,
"comments": 0,
"created_at": "2024-12-01T16:00:00Z",
"updated_at": "2024-12-01T16:00:00Z",
"closed_at": null,
"author_association": "NONE",
"active_lock_reason": null,
"body": "## Description\n\nWhen I try to process a file larger than 100MB, the application crashes with an out of memory error.\n\n## Steps to Reproduce\n\n1. Open the application\n2. Select a file larger than 100MB\n3. Click 'Process'\n4. Application crashes\n\n## Expected Behavior\n\nThe application should handle large files gracefully.\n\n## Environment\n\n- OS: Ubuntu 22.04\n- Python: 3.11\n- AutoGPT Version: 1.0.0",
"reactions": {
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/issues/12345/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/issues/12345/timeline",
"state_reason": null
},
"repository": {
"id": 614765452,
"node_id": "R_kgDOJKSTjA",
"name": "AutoGPT",
"full_name": "Significant-Gravitas/AutoGPT",
"private": false,
"owner": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"url": "https://api.github.com/users/Significant-Gravitas",
"html_url": "https://github.com/Significant-Gravitas",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Significant-Gravitas/AutoGPT",
"description": "AutoGPT is the vision of accessible AI for everyone, to use and to build on.",
"fork": false,
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT",
"created_at": "2023-03-16T09:21:07Z",
"updated_at": "2024-12-01T16:00:00Z",
"pushed_at": "2024-12-01T12:00:00Z",
"stargazers_count": 170000,
"watchers_count": 170000,
"language": "Python",
"forks_count": 45000,
"open_issues_count": 190,
"visibility": "public",
"default_branch": "master"
},
"organization": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"url": "https://api.github.com/orgs/Significant-Gravitas",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"description": ""
},
"sender": {
"login": "bug-reporter",
"id": 11111111,
"node_id": "MDQ6VXNlcjExMTExMTEx",
"avatar_url": "https://avatars.githubusercontent.com/u/11111111?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bug-reporter",
"html_url": "https://github.com/bug-reporter",
"type": "User",
"site_admin": false
}
}

View File

@@ -0,0 +1,97 @@
{
"action": "published",
"release": {
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/releases/123456789",
"assets_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/releases/123456789/assets",
"upload_url": "https://uploads.github.com/repos/Significant-Gravitas/AutoGPT/releases/123456789/assets{?name,label}",
"html_url": "https://github.com/Significant-Gravitas/AutoGPT/releases/tag/v1.0.0",
"id": 123456789,
"author": {
"login": "ntindle",
"id": 12345678,
"node_id": "MDQ6VXNlcjEyMzQ1Njc4",
"avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/ntindle",
"html_url": "https://github.com/ntindle",
"type": "User",
"site_admin": false
},
"node_id": "RE_kwDOJKSTjM4HWwAA",
"tag_name": "v1.0.0",
"target_commitish": "master",
"name": "AutoGPT Platform v1.0.0",
"draft": false,
"prerelease": false,
"created_at": "2024-12-01T10:00:00Z",
"published_at": "2024-12-01T12:00:00Z",
"assets": [
{
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/releases/assets/987654321",
"id": 987654321,
"node_id": "RA_kwDOJKSTjM4HWwBB",
"name": "autogpt-v1.0.0.zip",
"label": "Release Package",
"content_type": "application/zip",
"state": "uploaded",
"size": 52428800,
"download_count": 0,
"created_at": "2024-12-01T11:30:00Z",
"updated_at": "2024-12-01T11:35:00Z",
"browser_download_url": "https://github.com/Significant-Gravitas/AutoGPT/releases/download/v1.0.0/autogpt-v1.0.0.zip"
}
],
"tarball_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/tarball/v1.0.0",
"zipball_url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT/zipball/v1.0.0",
"body": "## What's New\n\n- Feature 1: Amazing new capability\n- Feature 2: Performance improvements\n- Bug fixes and stability improvements\n\n## Breaking Changes\n\nNone\n\n## Contributors\n\nThanks to all our contributors!"
},
"repository": {
"id": 614765452,
"node_id": "R_kgDOJKSTjA",
"name": "AutoGPT",
"full_name": "Significant-Gravitas/AutoGPT",
"private": false,
"owner": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"url": "https://api.github.com/users/Significant-Gravitas",
"html_url": "https://github.com/Significant-Gravitas",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Significant-Gravitas/AutoGPT",
"description": "AutoGPT is the vision of accessible AI for everyone, to use and to build on.",
"fork": false,
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT",
"created_at": "2023-03-16T09:21:07Z",
"updated_at": "2024-12-01T12:00:00Z",
"pushed_at": "2024-12-01T12:00:00Z",
"stargazers_count": 170000,
"watchers_count": 170000,
"language": "Python",
"forks_count": 45000,
"visibility": "public",
"default_branch": "master"
},
"organization": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"url": "https://api.github.com/orgs/Significant-Gravitas",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"description": ""
},
"sender": {
"login": "ntindle",
"id": 12345678,
"node_id": "MDQ6VXNlcjEyMzQ1Njc4",
"avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/ntindle",
"html_url": "https://github.com/ntindle",
"type": "User",
"site_admin": false
}
}

View File

@@ -0,0 +1,53 @@
{
"action": "created",
"starred_at": "2024-12-01T15:30:00Z",
"repository": {
"id": 614765452,
"node_id": "R_kgDOJKSTjA",
"name": "AutoGPT",
"full_name": "Significant-Gravitas/AutoGPT",
"private": false,
"owner": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"url": "https://api.github.com/users/Significant-Gravitas",
"html_url": "https://github.com/Significant-Gravitas",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Significant-Gravitas/AutoGPT",
"description": "AutoGPT is the vision of accessible AI for everyone, to use and to build on.",
"fork": false,
"url": "https://api.github.com/repos/Significant-Gravitas/AutoGPT",
"created_at": "2023-03-16T09:21:07Z",
"updated_at": "2024-12-01T15:30:00Z",
"pushed_at": "2024-12-01T12:00:00Z",
"stargazers_count": 170001,
"watchers_count": 170001,
"language": "Python",
"forks_count": 45000,
"visibility": "public",
"default_branch": "master"
},
"organization": {
"login": "Significant-Gravitas",
"id": 130738209,
"node_id": "O_kgDOB8roIQ",
"url": "https://api.github.com/orgs/Significant-Gravitas",
"avatar_url": "https://avatars.githubusercontent.com/u/130738209?v=4",
"description": ""
},
"sender": {
"login": "awesome-contributor",
"id": 98765432,
"node_id": "MDQ6VXNlcjk4NzY1NDMy",
"avatar_url": "https://avatars.githubusercontent.com/u/98765432?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/awesome-contributor",
"html_url": "https://github.com/awesome-contributor",
"type": "User",
"site_admin": false
}
}

View File

@@ -159,3 +159,391 @@ class GithubPullRequestTriggerBlock(GitHubTriggerBase, Block):
# --8<-- [end:GithubTriggerExample]
class GithubStarTriggerBlock(GitHubTriggerBase, Block):
"""Trigger block for GitHub star events - useful for milestone celebrations."""
EXAMPLE_PAYLOAD_FILE = (
Path(__file__).parent / "example_payloads" / "star.created.json"
)
class Input(GitHubTriggerBase.Input):
class EventsFilter(BaseModel):
"""
https://docs.github.com/en/webhooks/webhook-events-and-payloads#star
"""
created: bool = False
deleted: bool = False
events: EventsFilter = SchemaField(
title="Events", description="The star events to subscribe to"
)
class Output(GitHubTriggerBase.Output):
event: str = SchemaField(
description="The star event that triggered the webhook ('created' or 'deleted')"
)
starred_at: str = SchemaField(
description="ISO timestamp when the repo was starred (empty if deleted)"
)
stargazers_count: int = SchemaField(
description="Current number of stars on the repository"
)
repository_name: str = SchemaField(
description="Full name of the repository (owner/repo)"
)
repository_url: str = SchemaField(description="URL to the repository")
def __init__(self):
from backend.integrations.webhooks.github import GithubWebhookType
example_payload = json.loads(
self.EXAMPLE_PAYLOAD_FILE.read_text(encoding="utf-8")
)
super().__init__(
id="551e0a35-100b-49b7-89b8-3031322239b6",
description="This block triggers on GitHub star events. "
"Useful for celebrating milestones (e.g., 1k, 10k stars) or tracking engagement.",
categories={BlockCategory.DEVELOPER_TOOLS, BlockCategory.INPUT},
input_schema=GithubStarTriggerBlock.Input,
output_schema=GithubStarTriggerBlock.Output,
webhook_config=BlockWebhookConfig(
provider=ProviderName.GITHUB,
webhook_type=GithubWebhookType.REPO,
resource_format="{repo}",
event_filter_input="events",
event_format="star.{event}",
),
test_input={
"repo": "Significant-Gravitas/AutoGPT",
"events": {"created": True},
"credentials": TEST_CREDENTIALS_INPUT,
"payload": example_payload,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("payload", example_payload),
("triggered_by_user", example_payload["sender"]),
("event", example_payload["action"]),
("starred_at", example_payload.get("starred_at", "")),
("stargazers_count", example_payload["repository"]["stargazers_count"]),
("repository_name", example_payload["repository"]["full_name"]),
("repository_url", example_payload["repository"]["html_url"]),
],
)
async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
async for name, value in super().run(input_data, **kwargs):
yield name, value
yield "event", input_data.payload["action"]
yield "starred_at", input_data.payload.get("starred_at", "")
yield "stargazers_count", input_data.payload["repository"]["stargazers_count"]
yield "repository_name", input_data.payload["repository"]["full_name"]
yield "repository_url", input_data.payload["repository"]["html_url"]
class GithubReleaseTriggerBlock(GitHubTriggerBase, Block):
"""Trigger block for GitHub release events - ideal for announcing new versions."""
EXAMPLE_PAYLOAD_FILE = (
Path(__file__).parent / "example_payloads" / "release.published.json"
)
class Input(GitHubTriggerBase.Input):
class EventsFilter(BaseModel):
"""
https://docs.github.com/en/webhooks/webhook-events-and-payloads#release
"""
published: bool = False
unpublished: bool = False
created: bool = False
edited: bool = False
deleted: bool = False
prereleased: bool = False
released: bool = False
events: EventsFilter = SchemaField(
title="Events", description="The release events to subscribe to"
)
class Output(GitHubTriggerBase.Output):
event: str = SchemaField(
description="The release event that triggered the webhook (e.g., 'published')"
)
release: dict = SchemaField(description="The full release object")
release_url: str = SchemaField(description="URL to the release page")
tag_name: str = SchemaField(description="The release tag name (e.g., 'v1.0.0')")
release_name: str = SchemaField(description="Human-readable release name")
body: str = SchemaField(description="Release notes/description")
prerelease: bool = SchemaField(description="Whether this is a prerelease")
draft: bool = SchemaField(description="Whether this is a draft release")
assets: list = SchemaField(description="List of release assets/files")
def __init__(self):
from backend.integrations.webhooks.github import GithubWebhookType
example_payload = json.loads(
self.EXAMPLE_PAYLOAD_FILE.read_text(encoding="utf-8")
)
super().__init__(
id="2052dd1b-74e1-46ac-9c87-c7a0e057b60b",
description="This block triggers on GitHub release events. "
"Perfect for automating announcements to Discord, Twitter, or other platforms.",
categories={BlockCategory.DEVELOPER_TOOLS, BlockCategory.INPUT},
input_schema=GithubReleaseTriggerBlock.Input,
output_schema=GithubReleaseTriggerBlock.Output,
webhook_config=BlockWebhookConfig(
provider=ProviderName.GITHUB,
webhook_type=GithubWebhookType.REPO,
resource_format="{repo}",
event_filter_input="events",
event_format="release.{event}",
),
test_input={
"repo": "Significant-Gravitas/AutoGPT",
"events": {"published": True},
"credentials": TEST_CREDENTIALS_INPUT,
"payload": example_payload,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("payload", example_payload),
("triggered_by_user", example_payload["sender"]),
("event", example_payload["action"]),
("release", example_payload["release"]),
("release_url", example_payload["release"]["html_url"]),
("tag_name", example_payload["release"]["tag_name"]),
("release_name", example_payload["release"]["name"]),
("body", example_payload["release"]["body"]),
("prerelease", example_payload["release"]["prerelease"]),
("draft", example_payload["release"]["draft"]),
("assets", example_payload["release"]["assets"]),
],
)
async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
async for name, value in super().run(input_data, **kwargs):
yield name, value
release = input_data.payload["release"]
yield "event", input_data.payload["action"]
yield "release", release
yield "release_url", release["html_url"]
yield "tag_name", release["tag_name"]
yield "release_name", release.get("name", "")
yield "body", release.get("body", "")
yield "prerelease", release["prerelease"]
yield "draft", release["draft"]
yield "assets", release["assets"]
class GithubIssuesTriggerBlock(GitHubTriggerBase, Block):
"""Trigger block for GitHub issues events - great for triage and notifications."""
EXAMPLE_PAYLOAD_FILE = (
Path(__file__).parent / "example_payloads" / "issues.opened.json"
)
class Input(GitHubTriggerBase.Input):
class EventsFilter(BaseModel):
"""
https://docs.github.com/en/webhooks/webhook-events-and-payloads#issues
"""
opened: bool = False
edited: bool = False
deleted: bool = False
closed: bool = False
reopened: bool = False
assigned: bool = False
unassigned: bool = False
labeled: bool = False
unlabeled: bool = False
locked: bool = False
unlocked: bool = False
transferred: bool = False
milestoned: bool = False
demilestoned: bool = False
pinned: bool = False
unpinned: bool = False
events: EventsFilter = SchemaField(
title="Events", description="The issue events to subscribe to"
)
class Output(GitHubTriggerBase.Output):
event: str = SchemaField(
description="The issue event that triggered the webhook (e.g., 'opened')"
)
number: int = SchemaField(description="The issue number")
issue: dict = SchemaField(description="The full issue object")
issue_url: str = SchemaField(description="URL to the issue")
issue_title: str = SchemaField(description="The issue title")
issue_body: str = SchemaField(description="The issue body/description")
labels: list = SchemaField(description="List of labels on the issue")
assignees: list = SchemaField(description="List of assignees")
state: str = SchemaField(description="Issue state ('open' or 'closed')")
def __init__(self):
from backend.integrations.webhooks.github import GithubWebhookType
example_payload = json.loads(
self.EXAMPLE_PAYLOAD_FILE.read_text(encoding="utf-8")
)
super().__init__(
id="b2605464-e486-4bf4-aad3-d8a213c8a48a",
description="This block triggers on GitHub issues events. "
"Useful for automated triage, notifications, and welcoming first-time contributors.",
categories={BlockCategory.DEVELOPER_TOOLS, BlockCategory.INPUT},
input_schema=GithubIssuesTriggerBlock.Input,
output_schema=GithubIssuesTriggerBlock.Output,
webhook_config=BlockWebhookConfig(
provider=ProviderName.GITHUB,
webhook_type=GithubWebhookType.REPO,
resource_format="{repo}",
event_filter_input="events",
event_format="issues.{event}",
),
test_input={
"repo": "Significant-Gravitas/AutoGPT",
"events": {"opened": True},
"credentials": TEST_CREDENTIALS_INPUT,
"payload": example_payload,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("payload", example_payload),
("triggered_by_user", example_payload["sender"]),
("event", example_payload["action"]),
("number", example_payload["issue"]["number"]),
("issue", example_payload["issue"]),
("issue_url", example_payload["issue"]["html_url"]),
("issue_title", example_payload["issue"]["title"]),
("issue_body", example_payload["issue"]["body"]),
("labels", example_payload["issue"]["labels"]),
("assignees", example_payload["issue"]["assignees"]),
("state", example_payload["issue"]["state"]),
],
)
async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
async for name, value in super().run(input_data, **kwargs):
yield name, value
issue = input_data.payload["issue"]
yield "event", input_data.payload["action"]
yield "number", issue["number"]
yield "issue", issue
yield "issue_url", issue["html_url"]
yield "issue_title", issue["title"]
yield "issue_body", issue.get("body") or ""
yield "labels", issue["labels"]
yield "assignees", issue["assignees"]
yield "state", issue["state"]
class GithubDiscussionTriggerBlock(GitHubTriggerBase, Block):
"""Trigger block for GitHub discussion events - perfect for community Q&A sync."""
EXAMPLE_PAYLOAD_FILE = (
Path(__file__).parent / "example_payloads" / "discussion.created.json"
)
class Input(GitHubTriggerBase.Input):
class EventsFilter(BaseModel):
"""
https://docs.github.com/en/webhooks/webhook-events-and-payloads#discussion
"""
created: bool = False
edited: bool = False
deleted: bool = False
answered: bool = False
unanswered: bool = False
labeled: bool = False
unlabeled: bool = False
locked: bool = False
unlocked: bool = False
category_changed: bool = False
transferred: bool = False
pinned: bool = False
unpinned: bool = False
events: EventsFilter = SchemaField(
title="Events", description="The discussion events to subscribe to"
)
class Output(GitHubTriggerBase.Output):
event: str = SchemaField(
description="The discussion event that triggered the webhook"
)
number: int = SchemaField(description="The discussion number")
discussion: dict = SchemaField(description="The full discussion object")
discussion_url: str = SchemaField(description="URL to the discussion")
title: str = SchemaField(description="The discussion title")
body: str = SchemaField(description="The discussion body")
category: dict = SchemaField(description="The discussion category object")
category_name: str = SchemaField(description="Name of the category")
state: str = SchemaField(description="Discussion state")
def __init__(self):
from backend.integrations.webhooks.github import GithubWebhookType
example_payload = json.loads(
self.EXAMPLE_PAYLOAD_FILE.read_text(encoding="utf-8")
)
super().__init__(
id="87f847b3-d81a-424e-8e89-acadb5c9d52b",
description="This block triggers on GitHub Discussions events. "
"Great for syncing Q&A to Discord or auto-responding to common questions. "
"Note: Discussions must be enabled on the repository.",
categories={BlockCategory.DEVELOPER_TOOLS, BlockCategory.INPUT},
input_schema=GithubDiscussionTriggerBlock.Input,
output_schema=GithubDiscussionTriggerBlock.Output,
webhook_config=BlockWebhookConfig(
provider=ProviderName.GITHUB,
webhook_type=GithubWebhookType.REPO,
resource_format="{repo}",
event_filter_input="events",
event_format="discussion.{event}",
),
test_input={
"repo": "Significant-Gravitas/AutoGPT",
"events": {"created": True},
"credentials": TEST_CREDENTIALS_INPUT,
"payload": example_payload,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("payload", example_payload),
("triggered_by_user", example_payload["sender"]),
("event", example_payload["action"]),
("number", example_payload["discussion"]["number"]),
("discussion", example_payload["discussion"]),
("discussion_url", example_payload["discussion"]["html_url"]),
("title", example_payload["discussion"]["title"]),
("body", example_payload["discussion"]["body"]),
("category", example_payload["discussion"]["category"]),
("category_name", example_payload["discussion"]["category"]["name"]),
("state", example_payload["discussion"]["state"]),
],
)
async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
async for name, value in super().run(input_data, **kwargs):
yield name, value
discussion = input_data.payload["discussion"]
yield "event", input_data.payload["action"]
yield "number", discussion["number"]
yield "discussion", discussion
yield "discussion_url", discussion["html_url"]
yield "title", discussion["title"]
yield "body", discussion.get("body") or ""
yield "category", discussion["category"]
yield "category_name", discussion["category"]["name"]
yield "state", discussion["state"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import logging
from typing import Any, Literal
from typing import Any
from prisma.enums import ReviewStatus
@@ -45,11 +45,11 @@ class HumanInTheLoopBlock(Block):
)
class Output(BlockSchemaOutput):
reviewed_data: Any = SchemaField(
description="The data after human review (may be modified)"
approved_data: Any = SchemaField(
description="The data when approved (may be modified by reviewer)"
)
status: Literal["approved", "rejected"] = SchemaField(
description="Status of the review: 'approved' or 'rejected'"
rejected_data: Any = SchemaField(
description="The data when rejected (may be modified by reviewer)"
)
review_message: str = SchemaField(
description="Any message provided by the reviewer", default=""
@@ -69,8 +69,7 @@ class HumanInTheLoopBlock(Block):
"editable": True,
},
test_output=[
("status", "approved"),
("reviewed_data", {"name": "John Doe", "age": 30}),
("approved_data", {"name": "John Doe", "age": 30}),
],
test_mock={
"get_or_create_human_review": lambda *_args, **_kwargs: ReviewResult(
@@ -116,8 +115,7 @@ class HumanInTheLoopBlock(Block):
logger.info(
f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
)
yield "status", "approved"
yield "reviewed_data", input_data.data
yield "approved_data", input_data.data
yield "review_message", "Auto-approved (safe mode disabled)"
return
@@ -158,12 +156,11 @@ class HumanInTheLoopBlock(Block):
)
if result.status == ReviewStatus.APPROVED:
yield "status", "approved"
yield "reviewed_data", result.data
yield "approved_data", result.data
if result.message:
yield "review_message", result.message
elif result.status == ReviewStatus.REJECTED:
yield "status", "rejected"
yield "rejected_data", result.data
if result.message:
yield "review_message", result.message

View File

@@ -1,8 +1,11 @@
import logging
import re
from collections import Counter
from concurrent.futures import Future
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel
import backend.blocks.llm as llm
from backend.blocks.agent import AgentExecutorBlock
from backend.data.block import (
@@ -20,16 +23,41 @@ from backend.data.dynamic_fields import (
is_dynamic_field,
is_tool_pin,
)
from backend.data.execution import ExecutionContext
from backend.data.model import NodeExecutionStats, SchemaField
from backend.util import json
from backend.util.clients import get_database_manager_async_client
from backend.util.prompt import MAIN_OBJECTIVE_PREFIX
if TYPE_CHECKING:
from backend.data.graph import Link, Node
from backend.executor.manager import ExecutionProcessor
logger = logging.getLogger(__name__)
class ToolInfo(BaseModel):
"""Processed tool call information."""
tool_call: Any # The original tool call object from LLM response
tool_name: str # The function name
tool_def: dict[str, Any] # The tool definition from tool_functions
input_data: dict[str, Any] # Processed input data ready for tool execution
field_mapping: dict[str, str] # Field name mapping for the tool
class ExecutionParams(BaseModel):
"""Tool execution parameters."""
user_id: str
graph_id: str
node_id: str
graph_version: int
graph_exec_id: str
node_exec_id: str
execution_context: "ExecutionContext"
def _get_tool_requests(entry: dict[str, Any]) -> list[str]:
"""
Return a list of tool_call_ids if the entry is a tool request.
@@ -105,6 +133,50 @@ def _create_tool_response(call_id: str, output: Any) -> dict[str, Any]:
return {"role": "tool", "tool_call_id": call_id, "content": content}
def _combine_tool_responses(tool_outputs: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Combine multiple Anthropic tool responses into a single user message.
For non-Anthropic formats, returns the original list unchanged.
"""
if len(tool_outputs) <= 1:
return tool_outputs
# Anthropic responses have role="user", type="message", and content is a list with tool_result items
anthropic_responses = [
output
for output in tool_outputs
if (
output.get("role") == "user"
and output.get("type") == "message"
and isinstance(output.get("content"), list)
and any(
item.get("type") == "tool_result"
for item in output.get("content", [])
if isinstance(item, dict)
)
)
]
if len(anthropic_responses) > 1:
combined_content = [
item for response in anthropic_responses for item in response["content"]
]
combined_response = {
"role": "user",
"type": "message",
"content": combined_content,
}
non_anthropic_responses = [
output for output in tool_outputs if output not in anthropic_responses
]
return [combined_response] + non_anthropic_responses
return tool_outputs
def _convert_raw_response_to_dict(raw_response: Any) -> dict[str, Any]:
"""
Safely convert raw_response to dictionary format for conversation history.
@@ -204,6 +276,17 @@ class SmartDecisionMakerBlock(Block):
default="localhost:11434",
description="Ollama host for local models",
)
agent_mode_max_iterations: int = SchemaField(
title="Agent Mode Max Iterations",
description="Maximum iterations for agent mode. 0 = traditional mode (single LLM call, yield tool calls for external execution), -1 = infinite agent mode (loop until finished), 1+ = agent mode with max iterations limit.",
advanced=True,
default=0,
)
conversation_compaction: bool = SchemaField(
default=True,
title="Context window auto-compaction",
description="Automatically compact the context window once it hits the limit",
)
@classmethod
def get_missing_links(cls, data: BlockInput, links: list["Link"]) -> set[str]:
@@ -506,6 +589,7 @@ class SmartDecisionMakerBlock(Block):
Returns the response if successful, raises ValueError if validation fails.
"""
resp = await llm.llm_call(
compress_prompt_to_fit=input_data.conversation_compaction,
credentials=credentials,
llm_model=input_data.model,
prompt=current_prompt,
@@ -593,6 +677,291 @@ class SmartDecisionMakerBlock(Block):
return resp
def _process_tool_calls(
self, response, tool_functions: list[dict[str, Any]]
) -> list[ToolInfo]:
"""Process tool calls and extract tool definitions, arguments, and input data.
Returns a list of tool info dicts with:
- tool_call: The original tool call object
- tool_name: The function name
- tool_def: The tool definition from tool_functions
- input_data: Processed input data dict (includes None values)
- field_mapping: Field name mapping for the tool
"""
if not response.tool_calls:
return []
processed_tools = []
for tool_call in response.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
tool_def = next(
(
tool
for tool in tool_functions
if tool["function"]["name"] == tool_name
),
None,
)
if not tool_def:
if len(tool_functions) == 1:
tool_def = tool_functions[0]
else:
continue
# Build input data for the tool
input_data = {}
field_mapping = tool_def["function"].get("_field_mapping", {})
if "function" in tool_def and "parameters" in tool_def["function"]:
expected_args = tool_def["function"]["parameters"].get("properties", {})
for clean_arg_name in expected_args:
original_field_name = field_mapping.get(
clean_arg_name, clean_arg_name
)
arg_value = tool_args.get(clean_arg_name)
# Include all expected parameters, even if None (for backward compatibility with tests)
input_data[original_field_name] = arg_value
processed_tools.append(
ToolInfo(
tool_call=tool_call,
tool_name=tool_name,
tool_def=tool_def,
input_data=input_data,
field_mapping=field_mapping,
)
)
return processed_tools
def _update_conversation(
self, prompt: list[dict], response, tool_outputs: list | None = None
):
"""Update conversation history with response and tool outputs."""
# Don't add separate reasoning message with tool calls (breaks Anthropic's tool_use->tool_result pairing)
assistant_message = _convert_raw_response_to_dict(response.raw_response)
has_tool_calls = isinstance(assistant_message.get("content"), list) and any(
item.get("type") == "tool_use"
for item in assistant_message.get("content", [])
)
if response.reasoning and not has_tool_calls:
prompt.append(
{"role": "assistant", "content": f"[Reasoning]: {response.reasoning}"}
)
prompt.append(assistant_message)
if tool_outputs:
prompt.extend(tool_outputs)
async def _execute_single_tool_with_manager(
self,
tool_info: ToolInfo,
execution_params: ExecutionParams,
execution_processor: "ExecutionProcessor",
) -> dict:
"""Execute a single tool using the execution manager for proper integration."""
# Lazy imports to avoid circular dependencies
from backend.data.execution import NodeExecutionEntry
tool_call = tool_info.tool_call
tool_def = tool_info.tool_def
raw_input_data = tool_info.input_data
# Get sink node and field mapping
sink_node_id = tool_def["function"]["_sink_node_id"]
# Use proper database operations for tool execution
db_client = get_database_manager_async_client()
# Get target node
target_node = await db_client.get_node(sink_node_id)
if not target_node:
raise ValueError(f"Target node {sink_node_id} not found")
# Create proper node execution using upsert_execution_input
node_exec_result = None
final_input_data = None
# Add all inputs to the execution
if not raw_input_data:
raise ValueError(f"Tool call has no input data: {tool_call}")
for input_name, input_value in raw_input_data.items():
node_exec_result, final_input_data = await db_client.upsert_execution_input(
node_id=sink_node_id,
graph_exec_id=execution_params.graph_exec_id,
input_name=input_name,
input_data=input_value,
)
assert node_exec_result is not None, "node_exec_result should not be None"
# Create NodeExecutionEntry for execution manager
node_exec_entry = NodeExecutionEntry(
user_id=execution_params.user_id,
graph_exec_id=execution_params.graph_exec_id,
graph_id=execution_params.graph_id,
graph_version=execution_params.graph_version,
node_exec_id=node_exec_result.node_exec_id,
node_id=sink_node_id,
block_id=target_node.block_id,
inputs=final_input_data or {},
execution_context=execution_params.execution_context,
)
# Use the execution manager to execute the tool node
try:
# Get NodeExecutionProgress from the execution manager's running nodes
node_exec_progress = execution_processor.running_node_execution[
sink_node_id
]
# Use the execution manager's own graph stats
graph_stats_pair = (
execution_processor.execution_stats,
execution_processor.execution_stats_lock,
)
# Create a completed future for the task tracking system
node_exec_future = Future()
node_exec_progress.add_task(
node_exec_id=node_exec_result.node_exec_id,
task=node_exec_future,
)
# Execute the node directly since we're in the SmartDecisionMaker context
node_exec_future.set_result(
await execution_processor.on_node_execution(
node_exec=node_exec_entry,
node_exec_progress=node_exec_progress,
nodes_input_masks=None,
graph_stats_pair=graph_stats_pair,
)
)
# Get outputs from database after execution completes using database manager client
node_outputs = await db_client.get_execution_outputs_by_node_exec_id(
node_exec_result.node_exec_id
)
# Create tool response
tool_response_content = (
json.dumps(node_outputs)
if node_outputs
else "Tool executed successfully"
)
return _create_tool_response(tool_call.id, tool_response_content)
except Exception as e:
logger.error(f"Tool execution with manager failed: {e}")
# Return error response
return _create_tool_response(
tool_call.id, f"Tool execution failed: {str(e)}"
)
async def _execute_tools_agent_mode(
self,
input_data,
credentials,
tool_functions: list[dict[str, Any]],
prompt: list[dict],
graph_exec_id: str,
node_id: str,
node_exec_id: str,
user_id: str,
graph_id: str,
graph_version: int,
execution_context: ExecutionContext,
execution_processor: "ExecutionProcessor",
):
"""Execute tools in agent mode with a loop until finished."""
max_iterations = input_data.agent_mode_max_iterations
iteration = 0
# Execution parameters for tool execution
execution_params = ExecutionParams(
user_id=user_id,
graph_id=graph_id,
node_id=node_id,
graph_version=graph_version,
graph_exec_id=graph_exec_id,
node_exec_id=node_exec_id,
execution_context=execution_context,
)
current_prompt = list(prompt)
while max_iterations < 0 or iteration < max_iterations:
iteration += 1
logger.debug(f"Agent mode iteration {iteration}")
# Prepare prompt for this iteration
iteration_prompt = list(current_prompt)
# On the last iteration, add a special system message to encourage completion
if max_iterations > 0 and iteration == max_iterations:
last_iteration_message = {
"role": "system",
"content": f"{MAIN_OBJECTIVE_PREFIX}This is your last iteration ({iteration}/{max_iterations}). "
"Try to complete the task with the information you have. If you cannot fully complete it, "
"provide a summary of what you've accomplished and what remains to be done. "
"Prefer finishing with a clear response rather than making additional tool calls.",
}
iteration_prompt.append(last_iteration_message)
# Get LLM response
try:
response = await self._attempt_llm_call_with_validation(
credentials, input_data, iteration_prompt, tool_functions
)
except Exception as e:
yield "error", f"LLM call failed in agent mode iteration {iteration}: {str(e)}"
return
# Process tool calls
processed_tools = self._process_tool_calls(response, tool_functions)
# If no tool calls, we're done
if not processed_tools:
yield "finished", response.response
self._update_conversation(current_prompt, response)
yield "conversations", current_prompt
return
# Execute tools and collect responses
tool_outputs = []
for tool_info in processed_tools:
try:
tool_response = await self._execute_single_tool_with_manager(
tool_info, execution_params, execution_processor
)
tool_outputs.append(tool_response)
except Exception as e:
logger.error(f"Tool execution failed: {e}")
# Create error response for the tool
error_response = _create_tool_response(
tool_info.tool_call.id, f"Error: {str(e)}"
)
tool_outputs.append(error_response)
tool_outputs = _combine_tool_responses(tool_outputs)
self._update_conversation(current_prompt, response, tool_outputs)
# Yield intermediate conversation state
yield "conversations", current_prompt
# If we reach max iterations, yield the current state
if max_iterations < 0:
yield "finished", f"Agent mode completed after {iteration} iterations"
else:
yield "finished", f"Agent mode completed after {max_iterations} iterations (limit reached)"
yield "conversations", current_prompt
async def run(
self,
input_data: Input,
@@ -603,8 +972,12 @@ class SmartDecisionMakerBlock(Block):
graph_exec_id: str,
node_exec_id: str,
user_id: str,
graph_version: int,
execution_context: ExecutionContext,
execution_processor: "ExecutionProcessor",
**kwargs,
) -> BlockOutput:
tool_functions = await self._create_tool_node_signatures(node_id)
yield "tool_functions", json.dumps(tool_functions)
@@ -648,24 +1021,52 @@ class SmartDecisionMakerBlock(Block):
input_data.prompt = llm.fmt.format_string(input_data.prompt, values)
input_data.sys_prompt = llm.fmt.format_string(input_data.sys_prompt, values)
prefix = "[Main Objective Prompt]: "
if input_data.sys_prompt and not any(
p["role"] == "system" and p["content"].startswith(prefix) for p in prompt
p["role"] == "system" and p["content"].startswith(MAIN_OBJECTIVE_PREFIX)
for p in prompt
):
prompt.append({"role": "system", "content": prefix + input_data.sys_prompt})
prompt.append(
{
"role": "system",
"content": MAIN_OBJECTIVE_PREFIX + input_data.sys_prompt,
}
)
if input_data.prompt and not any(
p["role"] == "user" and p["content"].startswith(prefix) for p in prompt
p["role"] == "user" and p["content"].startswith(MAIN_OBJECTIVE_PREFIX)
for p in prompt
):
prompt.append({"role": "user", "content": prefix + input_data.prompt})
prompt.append(
{"role": "user", "content": MAIN_OBJECTIVE_PREFIX + input_data.prompt}
)
# Execute tools based on the selected mode
if input_data.agent_mode_max_iterations != 0:
# In agent mode, execute tools directly in a loop until finished
async for result in self._execute_tools_agent_mode(
input_data=input_data,
credentials=credentials,
tool_functions=tool_functions,
prompt=prompt,
graph_exec_id=graph_exec_id,
node_id=node_id,
node_exec_id=node_exec_id,
user_id=user_id,
graph_id=graph_id,
graph_version=graph_version,
execution_context=execution_context,
execution_processor=execution_processor,
):
yield result
return
# One-off mode: single LLM call and yield tool calls for external execution
current_prompt = list(prompt)
max_attempts = max(1, int(input_data.retry))
response = None
last_error = None
for attempt in range(max_attempts):
for _ in range(max_attempts):
try:
response = await self._attempt_llm_call_with_validation(
credentials, input_data, current_prompt, tool_functions

View File

@@ -1,7 +1,11 @@
import logging
import threading
from collections import defaultdict
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from backend.data.execution import ExecutionContext
from backend.data.model import ProviderName, User
from backend.server.model import CreateGraph
from backend.server.rest_api import AgentServer
@@ -17,10 +21,10 @@ async def create_graph(s: SpinTestServer, g, u: User):
async def create_credentials(s: SpinTestServer, u: User):
import backend.blocks.llm as llm
import backend.blocks.llm as llm_module
provider = ProviderName.OPENAI
credentials = llm.TEST_CREDENTIALS
credentials = llm_module.TEST_CREDENTIALS
return await s.agent_server.test_create_credentials(u.id, provider, credentials)
@@ -196,8 +200,6 @@ async def test_smart_decision_maker_function_signature(server: SpinTestServer):
@pytest.mark.asyncio
async def test_smart_decision_maker_tracks_llm_stats():
"""Test that SmartDecisionMakerBlock correctly tracks LLM usage stats."""
from unittest.mock import MagicMock, patch
import backend.blocks.llm as llm_module
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
@@ -216,7 +218,6 @@ async def test_smart_decision_maker_tracks_llm_stats():
}
# Mock the _create_tool_node_signatures method to avoid database calls
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
@@ -234,10 +235,19 @@ async def test_smart_decision_maker_tracks_llm_stats():
prompt="Should I continue with this task?",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0,
)
# Execute the block
outputs = {}
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
@@ -246,6 +256,9 @@ async def test_smart_decision_maker_tracks_llm_stats():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -263,8 +276,6 @@ async def test_smart_decision_maker_tracks_llm_stats():
@pytest.mark.asyncio
async def test_smart_decision_maker_parameter_validation():
"""Test that SmartDecisionMakerBlock correctly validates tool call parameters."""
from unittest.mock import MagicMock, patch
import backend.blocks.llm as llm_module
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
@@ -311,8 +322,6 @@ async def test_smart_decision_maker_parameter_validation():
mock_response_with_typo.reasoning = None
mock_response_with_typo.raw_response = {"role": "assistant", "content": None}
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
@@ -329,8 +338,17 @@ async def test_smart_decision_maker_parameter_validation():
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
retry=2, # Set retry to 2 for testing
agent_mode_max_iterations=0,
)
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
# Should raise ValueError after retries due to typo'd parameter name
with pytest.raises(ValueError) as exc_info:
outputs = {}
@@ -342,6 +360,9 @@ async def test_smart_decision_maker_parameter_validation():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -368,8 +389,6 @@ async def test_smart_decision_maker_parameter_validation():
mock_response_missing_required.reasoning = None
mock_response_missing_required.raw_response = {"role": "assistant", "content": None}
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
@@ -385,8 +404,17 @@ async def test_smart_decision_maker_parameter_validation():
prompt="Search for keywords",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0,
)
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
# Should raise ValueError due to missing required parameter
with pytest.raises(ValueError) as exc_info:
outputs = {}
@@ -398,6 +426,9 @@ async def test_smart_decision_maker_parameter_validation():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -418,8 +449,6 @@ async def test_smart_decision_maker_parameter_validation():
mock_response_valid.reasoning = None
mock_response_valid.raw_response = {"role": "assistant", "content": None}
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
@@ -435,10 +464,19 @@ async def test_smart_decision_maker_parameter_validation():
prompt="Search for keywords",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0,
)
# Should succeed - optional parameter missing is OK
outputs = {}
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
@@ -447,6 +485,9 @@ async def test_smart_decision_maker_parameter_validation():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -472,8 +513,6 @@ async def test_smart_decision_maker_parameter_validation():
mock_response_all_params.reasoning = None
mock_response_all_params.raw_response = {"role": "assistant", "content": None}
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
@@ -489,10 +528,19 @@ async def test_smart_decision_maker_parameter_validation():
prompt="Search for keywords",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0,
)
# Should succeed with all parameters
outputs = {}
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
@@ -501,6 +549,9 @@ async def test_smart_decision_maker_parameter_validation():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -513,8 +564,6 @@ async def test_smart_decision_maker_parameter_validation():
@pytest.mark.asyncio
async def test_smart_decision_maker_raw_response_conversion():
"""Test that SmartDecisionMaker correctly handles different raw_response types with retry mechanism."""
from unittest.mock import MagicMock, patch
import backend.blocks.llm as llm_module
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
@@ -584,7 +633,6 @@ async def test_smart_decision_maker_raw_response_conversion():
)
# Mock llm_call to return different responses on different calls
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call", new_callable=AsyncMock
@@ -603,10 +651,19 @@ async def test_smart_decision_maker_raw_response_conversion():
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
retry=2,
agent_mode_max_iterations=0,
)
# Should succeed after retry, demonstrating our helper function works
outputs = {}
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
@@ -615,6 +672,9 @@ async def test_smart_decision_maker_raw_response_conversion():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -650,8 +710,6 @@ async def test_smart_decision_maker_raw_response_conversion():
"I'll help you with that." # Ollama returns string
)
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
@@ -666,9 +724,18 @@ async def test_smart_decision_maker_raw_response_conversion():
prompt="Simple prompt",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0,
)
outputs = {}
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
@@ -677,6 +744,9 @@ async def test_smart_decision_maker_raw_response_conversion():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
@@ -696,8 +766,6 @@ async def test_smart_decision_maker_raw_response_conversion():
"content": "Test response",
} # Dict format
from unittest.mock import AsyncMock
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
@@ -712,6 +780,160 @@ async def test_smart_decision_maker_raw_response_conversion():
prompt="Another test",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0,
)
outputs = {}
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
graph_id="test-graph-id",
node_id="test-node-id",
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
assert "finished" in outputs
assert outputs["finished"] == "Test response"
@pytest.mark.asyncio
async def test_smart_decision_maker_agent_mode():
"""Test that agent mode executes tools directly and loops until finished."""
import backend.blocks.llm as llm_module
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
block = SmartDecisionMakerBlock()
# Mock tool call that requires multiple iterations
mock_tool_call_1 = MagicMock()
mock_tool_call_1.id = "call_1"
mock_tool_call_1.function.name = "search_keywords"
mock_tool_call_1.function.arguments = (
'{"query": "test", "max_keyword_difficulty": 50}'
)
mock_response_1 = MagicMock()
mock_response_1.response = None
mock_response_1.tool_calls = [mock_tool_call_1]
mock_response_1.prompt_tokens = 50
mock_response_1.completion_tokens = 25
mock_response_1.reasoning = "Using search tool"
mock_response_1.raw_response = {
"role": "assistant",
"content": None,
"tool_calls": [{"id": "call_1", "type": "function"}],
}
# Final response with no tool calls (finished)
mock_response_2 = MagicMock()
mock_response_2.response = "Task completed successfully"
mock_response_2.tool_calls = []
mock_response_2.prompt_tokens = 30
mock_response_2.completion_tokens = 15
mock_response_2.reasoning = None
mock_response_2.raw_response = {
"role": "assistant",
"content": "Task completed successfully",
}
# Mock the LLM call to return different responses on each iteration
llm_call_mock = AsyncMock()
llm_call_mock.side_effect = [mock_response_1, mock_response_2]
# Mock tool node signatures
mock_tool_signatures = [
{
"type": "function",
"function": {
"name": "search_keywords",
"_sink_node_id": "test-sink-node-id",
"_field_mapping": {},
"parameters": {
"properties": {
"query": {"type": "string"},
"max_keyword_difficulty": {"type": "integer"},
},
"required": ["query", "max_keyword_difficulty"],
},
},
}
]
# Mock database and execution components
mock_db_client = AsyncMock()
mock_node = MagicMock()
mock_node.block_id = "test-block-id"
mock_db_client.get_node.return_value = mock_node
# Mock upsert_execution_input to return proper NodeExecutionResult and input data
mock_node_exec_result = MagicMock()
mock_node_exec_result.node_exec_id = "test-tool-exec-id"
mock_input_data = {"query": "test", "max_keyword_difficulty": 50}
mock_db_client.upsert_execution_input.return_value = (
mock_node_exec_result,
mock_input_data,
)
# No longer need mock_execute_node since we use execution_processor.on_node_execution
with patch("backend.blocks.llm.llm_call", llm_call_mock), patch.object(
block, "_create_tool_node_signatures", return_value=mock_tool_signatures
), patch(
"backend.blocks.smart_decision_maker.get_database_manager_async_client",
return_value=mock_db_client,
), patch(
"backend.executor.manager.async_update_node_execution_status",
new_callable=AsyncMock,
), patch(
"backend.integrations.creds_manager.IntegrationCredentialsManager"
):
# Create a mock execution context
mock_execution_context = ExecutionContext(
safe_mode=False,
)
# Create a mock execution processor for agent mode tests
mock_execution_processor = AsyncMock()
# Configure the execution processor mock with required attributes
mock_execution_processor.running_node_execution = defaultdict(MagicMock)
mock_execution_processor.execution_stats = MagicMock()
mock_execution_processor.execution_stats_lock = threading.Lock()
# Mock the on_node_execution method to return successful stats
mock_node_stats = MagicMock()
mock_node_stats.error = None # No error
mock_execution_processor.on_node_execution = AsyncMock(
return_value=mock_node_stats
)
# Mock the get_execution_outputs_by_node_exec_id method
mock_db_client.get_execution_outputs_by_node_exec_id.return_value = {
"result": {"status": "success", "data": "search completed"}
}
# Test agent mode with max_iterations = 3
input_data = SmartDecisionMakerBlock.Input(
prompt="Complete this task using tools",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=3, # Enable agent mode with 3 max iterations
)
outputs = {}
@@ -723,8 +945,115 @@ async def test_smart_decision_maker_raw_response_conversion():
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
# Verify agent mode behavior
assert "tool_functions" in outputs # tool_functions is yielded in both modes
assert "finished" in outputs
assert outputs["finished"] == "Test response"
assert outputs["finished"] == "Task completed successfully"
assert "conversations" in outputs
# Verify the conversation includes tool responses
conversations = outputs["conversations"]
assert len(conversations) > 2 # Should have multiple conversation entries
# Verify LLM was called twice (once for tool call, once for finish)
assert llm_call_mock.call_count == 2
# Verify tool was executed via execution processor
assert mock_execution_processor.on_node_execution.call_count == 1
@pytest.mark.asyncio
async def test_smart_decision_maker_traditional_mode_default():
"""Test that default behavior (agent_mode_max_iterations=0) works as traditional mode."""
import backend.blocks.llm as llm_module
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
block = SmartDecisionMakerBlock()
# Mock tool call
mock_tool_call = MagicMock()
mock_tool_call.function.name = "search_keywords"
mock_tool_call.function.arguments = (
'{"query": "test", "max_keyword_difficulty": 50}'
)
mock_response = MagicMock()
mock_response.response = None
mock_response.tool_calls = [mock_tool_call]
mock_response.prompt_tokens = 50
mock_response.completion_tokens = 25
mock_response.reasoning = None
mock_response.raw_response = {"role": "assistant", "content": None}
mock_tool_signatures = [
{
"type": "function",
"function": {
"name": "search_keywords",
"_sink_node_id": "test-sink-node-id",
"_field_mapping": {},
"parameters": {
"properties": {
"query": {"type": "string"},
"max_keyword_difficulty": {"type": "integer"},
},
"required": ["query", "max_keyword_difficulty"],
},
},
}
]
with patch(
"backend.blocks.llm.llm_call",
new_callable=AsyncMock,
return_value=mock_response,
), patch.object(
block, "_create_tool_node_signatures", return_value=mock_tool_signatures
):
# Test default behavior (traditional mode)
input_data = SmartDecisionMakerBlock.Input(
prompt="Test prompt",
model=llm_module.LlmModel.GPT4O,
credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore
agent_mode_max_iterations=0, # Traditional mode
)
# Create execution context
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a mock execution processor for tests
mock_execution_processor = MagicMock()
outputs = {}
async for output_name, output_data in block.run(
input_data,
credentials=llm_module.TEST_CREDENTIALS,
graph_id="test-graph-id",
node_id="test-node-id",
graph_exec_id="test-exec-id",
node_exec_id="test-node-exec-id",
user_id="test-user-id",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_data
# Verify traditional mode behavior
assert (
"tool_functions" in outputs
) # Should yield tool_functions in traditional mode
assert (
"tools_^_test-sink-node-id_~_query" in outputs
) # Should yield individual tool parameters
assert "tools_^_test-sink-node-id_~_max_keyword_difficulty" in outputs
assert "conversations" in outputs

View File

@@ -1,7 +1,7 @@
"""Comprehensive tests for SmartDecisionMakerBlock dynamic field handling."""
import json
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
@@ -308,10 +308,47 @@ async def test_output_yielding_with_dynamic_fields():
) as mock_llm:
mock_llm.return_value = mock_response
# Mock the function signature creation
with patch.object(
# Mock the database manager to avoid HTTP calls during tool execution
with patch(
"backend.blocks.smart_decision_maker.get_database_manager_async_client"
) as mock_db_manager, patch.object(
block, "_create_tool_node_signatures", new_callable=AsyncMock
) as mock_sig:
# Set up the mock database manager
mock_db_client = AsyncMock()
mock_db_manager.return_value = mock_db_client
# Mock the node retrieval
mock_target_node = Mock()
mock_target_node.id = "test-sink-node-id"
mock_target_node.block_id = "CreateDictionaryBlock"
mock_target_node.block = Mock()
mock_target_node.block.name = "Create Dictionary"
mock_db_client.get_node.return_value = mock_target_node
# Mock the execution result creation
mock_node_exec_result = Mock()
mock_node_exec_result.node_exec_id = "mock-node-exec-id"
mock_final_input_data = {
"values_#_name": "Alice",
"values_#_age": 30,
"values_#_email": "alice@example.com",
}
mock_db_client.upsert_execution_input.return_value = (
mock_node_exec_result,
mock_final_input_data,
)
# Mock the output retrieval
mock_outputs = {
"values_#_name": "Alice",
"values_#_age": 30,
"values_#_email": "alice@example.com",
}
mock_db_client.get_execution_outputs_by_node_exec_id.return_value = (
mock_outputs
)
mock_sig.return_value = [
{
"type": "function",
@@ -337,10 +374,16 @@ async def test_output_yielding_with_dynamic_fields():
prompt="Create a user dictionary",
credentials=llm.TEST_CREDENTIALS_INPUT,
model=llm.LlmModel.GPT4O,
agent_mode_max_iterations=0, # Use traditional mode to test output yielding
)
# Run the block
outputs = {}
from backend.data.execution import ExecutionContext
mock_execution_context = ExecutionContext(safe_mode=False)
mock_execution_processor = MagicMock()
async for output_name, output_value in block.run(
input_data,
credentials=llm.TEST_CREDENTIALS,
@@ -349,6 +392,9 @@ async def test_output_yielding_with_dynamic_fields():
graph_exec_id="test_exec",
node_exec_id="test_node_exec",
user_id="test_user",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_value
@@ -511,45 +557,108 @@ async def test_validation_errors_dont_pollute_conversation():
}
]
# Create input data
from backend.blocks import llm
# Mock the database manager to avoid HTTP calls during tool execution
with patch(
"backend.blocks.smart_decision_maker.get_database_manager_async_client"
) as mock_db_manager:
# Set up the mock database manager for agent mode
mock_db_client = AsyncMock()
mock_db_manager.return_value = mock_db_client
input_data = block.input_schema(
prompt="Test prompt",
credentials=llm.TEST_CREDENTIALS_INPUT,
model=llm.LlmModel.GPT4O,
retry=3, # Allow retries
)
# Mock the node retrieval
mock_target_node = Mock()
mock_target_node.id = "test-sink-node-id"
mock_target_node.block_id = "TestBlock"
mock_target_node.block = Mock()
mock_target_node.block.name = "Test Block"
mock_db_client.get_node.return_value = mock_target_node
# Run the block
outputs = {}
async for output_name, output_value in block.run(
input_data,
credentials=llm.TEST_CREDENTIALS,
graph_id="test_graph",
node_id="test_node",
graph_exec_id="test_exec",
node_exec_id="test_node_exec",
user_id="test_user",
):
outputs[output_name] = output_value
# Mock the execution result creation
mock_node_exec_result = Mock()
mock_node_exec_result.node_exec_id = "mock-node-exec-id"
mock_final_input_data = {"correct_param": "value"}
mock_db_client.upsert_execution_input.return_value = (
mock_node_exec_result,
mock_final_input_data,
)
# Verify we had 2 LLM calls (initial + retry)
assert call_count == 2
# Mock the output retrieval
mock_outputs = {"correct_param": "value"}
mock_db_client.get_execution_outputs_by_node_exec_id.return_value = (
mock_outputs
)
# Check the final conversation output
final_conversation = outputs.get("conversations", [])
# Create input data
from backend.blocks import llm
# The final conversation should NOT contain the validation error message
error_messages = [
msg
for msg in final_conversation
if msg.get("role") == "user"
and "parameter errors" in msg.get("content", "")
]
assert (
len(error_messages) == 0
), "Validation error leaked into final conversation"
input_data = block.input_schema(
prompt="Test prompt",
credentials=llm.TEST_CREDENTIALS_INPUT,
model=llm.LlmModel.GPT4O,
retry=3, # Allow retries
agent_mode_max_iterations=1,
)
# The final conversation should only have the successful response
assert final_conversation[-1]["content"] == "valid"
# Run the block
outputs = {}
from backend.data.execution import ExecutionContext
mock_execution_context = ExecutionContext(safe_mode=False)
# Create a proper mock execution processor for agent mode
from collections import defaultdict
mock_execution_processor = AsyncMock()
mock_execution_processor.execution_stats = MagicMock()
mock_execution_processor.execution_stats_lock = MagicMock()
# Create a mock NodeExecutionProgress for the sink node
mock_node_exec_progress = MagicMock()
mock_node_exec_progress.add_task = MagicMock()
mock_node_exec_progress.pop_output = MagicMock(
return_value=None
) # No outputs to process
# Set up running_node_execution as a defaultdict that returns our mock for any key
mock_execution_processor.running_node_execution = defaultdict(
lambda: mock_node_exec_progress
)
# Mock the on_node_execution method that gets called during tool execution
mock_node_stats = MagicMock()
mock_node_stats.error = None
mock_execution_processor.on_node_execution.return_value = (
mock_node_stats
)
async for output_name, output_value in block.run(
input_data,
credentials=llm.TEST_CREDENTIALS,
graph_id="test_graph",
node_id="test_node",
graph_exec_id="test_exec",
node_exec_id="test_node_exec",
user_id="test_user",
graph_version=1,
execution_context=mock_execution_context,
execution_processor=mock_execution_processor,
):
outputs[output_name] = output_value
# Verify we had at least 1 LLM call
assert call_count >= 1
# Check the final conversation output
final_conversation = outputs.get("conversations", [])
# The final conversation should NOT contain validation error messages
# Even if retries don't happen in agent mode, we should not leak errors
error_messages = [
msg
for msg in final_conversation
if msg.get("role") == "user"
and "parameter errors" in msg.get("content", "")
]
assert (
len(error_messages) == 0
), "Validation error leaked into final conversation"

View File

@@ -5,6 +5,7 @@ from enum import Enum
from multiprocessing import Manager
from queue import Empty
from typing import (
TYPE_CHECKING,
Annotated,
Any,
AsyncGenerator,
@@ -65,6 +66,9 @@ from .includes import (
)
from .model import CredentialsMetaInput, GraphExecutionStats, NodeExecutionStats
if TYPE_CHECKING:
pass
T = TypeVar("T")
logger = logging.getLogger(__name__)
@@ -836,6 +840,30 @@ async def upsert_execution_output(
await AgentNodeExecutionInputOutput.prisma().create(data=data)
async def get_execution_outputs_by_node_exec_id(
node_exec_id: str,
) -> dict[str, Any]:
"""
Get all execution outputs for a specific node execution ID.
Args:
node_exec_id: The node execution ID to get outputs for
Returns:
Dictionary mapping output names to their data values
"""
outputs = await AgentNodeExecutionInputOutput.prisma().find_many(
where={"referencedByOutputExecId": node_exec_id}
)
result = {}
for output in outputs:
if output.data is not None:
result[output.name] = type_utils.convert(output.data, JsonValue)
return result
async def update_graph_execution_start_time(
graph_exec_id: str,
) -> GraphExecution | None:

View File

@@ -100,7 +100,7 @@ async def get_or_create_human_review(
return None
else:
return ReviewResult(
data=review.payload if review.status == ReviewStatus.APPROVED else None,
data=review.payload,
status=review.status,
message=review.reviewMessage or "",
processed=review.processed,

View File

@@ -13,6 +13,7 @@ from backend.data.execution import (
get_block_error_stats,
get_child_graph_executions,
get_execution_kv_data,
get_execution_outputs_by_node_exec_id,
get_frequently_executed_graphs,
get_graph_execution_meta,
get_graph_executions,
@@ -147,6 +148,7 @@ class DatabaseManager(AppService):
update_graph_execution_stats = _(update_graph_execution_stats)
upsert_execution_input = _(upsert_execution_input)
upsert_execution_output = _(upsert_execution_output)
get_execution_outputs_by_node_exec_id = _(get_execution_outputs_by_node_exec_id)
get_execution_kv_data = _(get_execution_kv_data)
set_execution_kv_data = _(set_execution_kv_data)
get_block_error_stats = _(get_block_error_stats)
@@ -277,6 +279,7 @@ class DatabaseManagerAsyncClient(AppServiceClient):
get_user_integrations = d.get_user_integrations
upsert_execution_input = d.upsert_execution_input
upsert_execution_output = d.upsert_execution_output
get_execution_outputs_by_node_exec_id = d.get_execution_outputs_by_node_exec_id
update_graph_execution_stats = d.update_graph_execution_stats
update_node_execution_status = d.update_node_execution_status
update_node_execution_status_batch = d.update_node_execution_status_batch

View File

@@ -133,9 +133,8 @@ def execute_graph(
cluster_lock: ClusterLock,
):
"""Execute graph using thread-local ExecutionProcessor instance"""
return _tls.processor.on_graph_execution(
graph_exec_entry, cancel_event, cluster_lock
)
processor: ExecutionProcessor = _tls.processor
return processor.on_graph_execution(graph_exec_entry, cancel_event, cluster_lock)
T = TypeVar("T")
@@ -143,8 +142,8 @@ T = TypeVar("T")
async def execute_node(
node: Node,
creds_manager: IntegrationCredentialsManager,
data: NodeExecutionEntry,
execution_processor: "ExecutionProcessor",
execution_stats: NodeExecutionStats | None = None,
nodes_input_masks: Optional[NodesInputMasks] = None,
) -> BlockOutput:
@@ -169,6 +168,7 @@ async def execute_node(
node_id = data.node_id
node_block = node.block
execution_context = data.execution_context
creds_manager = execution_processor.creds_manager
log_metadata = LogMetadata(
logger=_logger,
@@ -212,6 +212,7 @@ async def execute_node(
"node_exec_id": node_exec_id,
"user_id": user_id,
"execution_context": execution_context,
"execution_processor": execution_processor,
}
# Last-minute fetch credentials + acquire a system-wide read-write lock to prevent
@@ -608,8 +609,8 @@ class ExecutionProcessor:
async for output_name, output_data in execute_node(
node=node,
creds_manager=self.creds_manager,
data=node_exec,
execution_processor=self,
execution_stats=stats,
nodes_input_masks=nodes_input_masks,
):
@@ -860,12 +861,17 @@ class ExecutionProcessor:
execution_stats_lock = threading.Lock()
# State holders ----------------------------------------------------
running_node_execution: dict[str, NodeExecutionProgress] = defaultdict(
self.running_node_execution: dict[str, NodeExecutionProgress] = defaultdict(
NodeExecutionProgress
)
running_node_evaluation: dict[str, Future] = {}
self.running_node_evaluation: dict[str, Future] = {}
self.execution_stats = execution_stats
self.execution_stats_lock = execution_stats_lock
execution_queue = ExecutionQueue[NodeExecutionEntry]()
running_node_execution = self.running_node_execution
running_node_evaluation = self.running_node_evaluation
try:
if db_client.get_credits(graph_exec.user_id) <= 0:
raise InsufficientBalanceError(

View File

@@ -1,9 +1,16 @@
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from difflib import SequenceMatcher
from typing import Sequence
import prisma
import backend.data.block
import backend.server.v2.library.db as library_db
import backend.server.v2.library.model as library_model
import backend.server.v2.store.db as store_db
import backend.server.v2.store.model as store_model
from backend.blocks import load_all_blocks
from backend.blocks.llm import LlmModel
from backend.data.block import AnyBlockSchema, BlockCategory, BlockInfo, BlockSchema
@@ -14,17 +21,36 @@ from backend.server.v2.builder.model import (
BlockResponse,
BlockType,
CountResponse,
FilterType,
Provider,
ProviderResponse,
SearchBlocksResponse,
SearchEntry,
)
from backend.util.cache import cached
from backend.util.models import Pagination
logger = logging.getLogger(__name__)
llm_models = [name.name.lower().replace("_", " ") for name in LlmModel]
_static_counts_cache: dict | None = None
_suggested_blocks: list[BlockInfo] | None = None
MAX_LIBRARY_AGENT_RESULTS = 100
MAX_MARKETPLACE_AGENT_RESULTS = 100
MIN_SCORE_FOR_FILTERED_RESULTS = 10.0
SearchResultItem = BlockInfo | library_model.LibraryAgent | store_model.StoreAgent
@dataclass
class _ScoredItem:
item: SearchResultItem
filter_type: FilterType
score: float
sort_key: str
@dataclass
class _SearchCacheEntry:
items: list[SearchResultItem]
total_items: dict[FilterType, int]
def get_block_categories(category_blocks: int = 3) -> list[BlockCategoryResponse]:
@@ -130,71 +156,244 @@ def get_block_by_id(block_id: str) -> BlockInfo | None:
return None
def search_blocks(
include_blocks: bool = True,
include_integrations: bool = True,
query: str = "",
page: int = 1,
page_size: int = 50,
) -> SearchBlocksResponse:
async def update_search(user_id: str, search: SearchEntry) -> str:
"""
Get blocks based on the filter and query.
`providers` only applies for `integrations` filter.
Upsert a search request for the user and return the search ID.
"""
blocks: list[AnyBlockSchema] = []
query = query.lower()
if search.search_id:
# Update existing search
await prisma.models.BuilderSearchHistory.prisma().update(
where={
"id": search.search_id,
},
data={
"searchQuery": search.search_query or "",
"filter": search.filter or [], # type: ignore
"byCreator": search.by_creator or [],
},
)
return search.search_id
else:
# Create new search
new_search = await prisma.models.BuilderSearchHistory.prisma().create(
data={
"userId": user_id,
"searchQuery": search.search_query or "",
"filter": search.filter or [], # type: ignore
"byCreator": search.by_creator or [],
}
)
return new_search.id
total = 0
skip = (page - 1) * page_size
take = page_size
async def get_recent_searches(user_id: str, limit: int = 5) -> list[SearchEntry]:
"""
Get the user's most recent search requests.
"""
searches = await prisma.models.BuilderSearchHistory.prisma().find_many(
where={
"userId": user_id,
},
order={
"updatedAt": "desc",
},
take=limit,
)
return [
SearchEntry(
search_query=s.searchQuery,
filter=s.filter, # type: ignore
by_creator=s.byCreator,
search_id=s.id,
)
for s in searches
]
async def get_sorted_search_results(
*,
user_id: str,
search_query: str | None,
filters: Sequence[FilterType],
by_creator: Sequence[str] | None = None,
) -> _SearchCacheEntry:
normalized_filters: tuple[FilterType, ...] = tuple(sorted(set(filters or [])))
normalized_creators: tuple[str, ...] = tuple(sorted(set(by_creator or [])))
return await _build_cached_search_results(
user_id=user_id,
search_query=search_query or "",
filters=normalized_filters,
by_creator=normalized_creators,
)
@cached(ttl_seconds=300, shared_cache=True)
async def _build_cached_search_results(
user_id: str,
search_query: str,
filters: tuple[FilterType, ...],
by_creator: tuple[str, ...],
) -> _SearchCacheEntry:
normalized_query = (search_query or "").strip().lower()
include_blocks = "blocks" in filters
include_integrations = "integrations" in filters
include_library_agents = "my_agents" in filters
include_marketplace_agents = "marketplace_agents" in filters
scored_items: list[_ScoredItem] = []
total_items: dict[FilterType, int] = {
"blocks": 0,
"integrations": 0,
"marketplace_agents": 0,
"my_agents": 0,
}
block_results, block_total, integration_total = _collect_block_results(
normalized_query=normalized_query,
include_blocks=include_blocks,
include_integrations=include_integrations,
)
scored_items.extend(block_results)
total_items["blocks"] = block_total
total_items["integrations"] = integration_total
if include_library_agents:
library_response = await library_db.list_library_agents(
user_id=user_id,
search_term=search_query or None,
page=1,
page_size=MAX_LIBRARY_AGENT_RESULTS,
)
total_items["my_agents"] = library_response.pagination.total_items
scored_items.extend(
_build_library_items(
agents=library_response.agents,
normalized_query=normalized_query,
)
)
if include_marketplace_agents:
marketplace_response = await store_db.get_store_agents(
creators=list(by_creator) or None,
search_query=search_query or None,
page=1,
page_size=MAX_MARKETPLACE_AGENT_RESULTS,
)
total_items["marketplace_agents"] = marketplace_response.pagination.total_items
scored_items.extend(
_build_marketplace_items(
agents=marketplace_response.agents,
normalized_query=normalized_query,
)
)
sorted_items = sorted(
scored_items,
key=lambda entry: (-entry.score, entry.sort_key, entry.filter_type),
)
return _SearchCacheEntry(
items=[entry.item for entry in sorted_items],
total_items=total_items,
)
def _collect_block_results(
*,
normalized_query: str,
include_blocks: bool,
include_integrations: bool,
) -> tuple[list[_ScoredItem], int, int]:
results: list[_ScoredItem] = []
block_count = 0
integration_count = 0
if not include_blocks and not include_integrations:
return results, block_count, integration_count
for block_type in load_all_blocks().values():
block: AnyBlockSchema = block_type()
# Skip disabled blocks
if block.disabled:
continue
# Skip blocks that don't match the query
if (
query not in block.name.lower()
and query not in block.description.lower()
and not _matches_llm_model(block.input_schema, query)
):
continue
keep = False
block_info = block.get_info()
credentials = list(block.input_schema.get_credentials_fields().values())
if include_integrations and len(credentials) > 0:
keep = True
is_integration = len(credentials) > 0
if is_integration and not include_integrations:
continue
if not is_integration and not include_blocks:
continue
score = _score_block(block, block_info, normalized_query)
if not _should_include_item(score, normalized_query):
continue
filter_type: FilterType = "integrations" if is_integration else "blocks"
if is_integration:
integration_count += 1
if include_blocks and len(credentials) == 0:
keep = True
else:
block_count += 1
if not keep:
results.append(
_ScoredItem(
item=block_info,
filter_type=filter_type,
score=score,
sort_key=_get_item_name(block_info),
)
)
return results, block_count, integration_count
def _build_library_items(
*,
agents: list[library_model.LibraryAgent],
normalized_query: str,
) -> list[_ScoredItem]:
results: list[_ScoredItem] = []
for agent in agents:
score = _score_library_agent(agent, normalized_query)
if not _should_include_item(score, normalized_query):
continue
total += 1
if skip > 0:
skip -= 1
continue
if take > 0:
take -= 1
blocks.append(block)
results.append(
_ScoredItem(
item=agent,
filter_type="my_agents",
score=score,
sort_key=_get_item_name(agent),
)
)
return SearchBlocksResponse(
blocks=BlockResponse(
blocks=[b.get_info() for b in blocks],
pagination=Pagination(
total_items=total,
total_pages=(total + page_size - 1) // page_size,
current_page=page,
page_size=page_size,
),
),
total_block_count=block_count,
total_integration_count=integration_count,
)
return results
def _build_marketplace_items(
*,
agents: list[store_model.StoreAgent],
normalized_query: str,
) -> list[_ScoredItem]:
results: list[_ScoredItem] = []
for agent in agents:
score = _score_store_agent(agent, normalized_query)
if not _should_include_item(score, normalized_query):
continue
results.append(
_ScoredItem(
item=agent,
filter_type="marketplace_agents",
score=score,
sort_key=_get_item_name(agent),
)
)
return results
def get_providers(
@@ -251,16 +450,12 @@ async def get_counts(user_id: str) -> CountResponse:
)
@cached(ttl_seconds=3600)
async def _get_static_counts():
"""
Get counts of blocks, integrations, and marketplace agents.
This is cached to avoid unnecessary database queries and calculations.
Can't use functools.cache here because the function is async.
"""
global _static_counts_cache
if _static_counts_cache is not None:
return _static_counts_cache
all_blocks = 0
input_blocks = 0
action_blocks = 0
@@ -287,7 +482,7 @@ async def _get_static_counts():
marketplace_agents = await prisma.models.StoreAgent.prisma().count()
_static_counts_cache = {
return {
"all_blocks": all_blocks,
"input_blocks": input_blocks,
"action_blocks": action_blocks,
@@ -296,8 +491,6 @@ async def _get_static_counts():
"marketplace_agents": marketplace_agents,
}
return _static_counts_cache
def _matches_llm_model(schema_cls: type[BlockSchema], query: str) -> bool:
for field in schema_cls.model_fields.values():
@@ -308,6 +501,123 @@ def _matches_llm_model(schema_cls: type[BlockSchema], query: str) -> bool:
return False
def _score_block(
block: AnyBlockSchema,
block_info: BlockInfo,
normalized_query: str,
) -> float:
if not normalized_query:
return 0.0
name = block_info.name.lower()
description = block_info.description.lower()
score = _score_primary_fields(name, description, normalized_query)
category_text = " ".join(
category.get("category", "").lower() for category in block_info.categories
)
score += _score_additional_field(category_text, normalized_query, 12, 6)
credentials_info = block.input_schema.get_credentials_fields_info().values()
provider_names = [
provider.value.lower()
for info in credentials_info
for provider in info.provider
]
provider_text = " ".join(provider_names)
score += _score_additional_field(provider_text, normalized_query, 15, 6)
if _matches_llm_model(block.input_schema, normalized_query):
score += 20
return score
def _score_library_agent(
agent: library_model.LibraryAgent,
normalized_query: str,
) -> float:
if not normalized_query:
return 0.0
name = agent.name.lower()
description = (agent.description or "").lower()
instructions = (agent.instructions or "").lower()
score = _score_primary_fields(name, description, normalized_query)
score += _score_additional_field(instructions, normalized_query, 15, 6)
score += _score_additional_field(
agent.creator_name.lower(), normalized_query, 10, 5
)
return score
def _score_store_agent(
agent: store_model.StoreAgent,
normalized_query: str,
) -> float:
if not normalized_query:
return 0.0
name = agent.agent_name.lower()
description = agent.description.lower()
sub_heading = agent.sub_heading.lower()
score = _score_primary_fields(name, description, normalized_query)
score += _score_additional_field(sub_heading, normalized_query, 12, 6)
score += _score_additional_field(agent.creator.lower(), normalized_query, 10, 5)
return score
def _score_primary_fields(name: str, description: str, query: str) -> float:
score = 0.0
if name == query:
score += 120
elif name.startswith(query):
score += 90
elif query in name:
score += 60
score += SequenceMatcher(None, name, query).ratio() * 50
if description:
if query in description:
score += 30
score += SequenceMatcher(None, description, query).ratio() * 25
return score
def _score_additional_field(
value: str,
query: str,
contains_weight: float,
similarity_weight: float,
) -> float:
if not value or not query:
return 0.0
score = 0.0
if query in value:
score += contains_weight
score += SequenceMatcher(None, value, query).ratio() * similarity_weight
return score
def _should_include_item(score: float, normalized_query: str) -> bool:
if not normalized_query:
return True
return score >= MIN_SCORE_FOR_FILTERED_RESULTS
def _get_item_name(item: SearchResultItem) -> str:
if isinstance(item, BlockInfo):
return item.name.lower()
if isinstance(item, library_model.LibraryAgent):
return item.name.lower()
return item.agent_name.lower()
@cached(ttl_seconds=3600)
def _get_all_providers() -> dict[ProviderName, Provider]:
providers: dict[ProviderName, Provider] = {}
@@ -329,13 +639,9 @@ def _get_all_providers() -> dict[ProviderName, Provider]:
return providers
@cached(ttl_seconds=3600)
async def get_suggested_blocks(count: int = 5) -> list[BlockInfo]:
global _suggested_blocks
if _suggested_blocks is not None and len(_suggested_blocks) >= count:
return _suggested_blocks[:count]
_suggested_blocks = []
suggested_blocks = []
# Sum the number of executions for each block type
# Prisma cannot group by nested relations, so we do a raw query
# Calculate the cutoff timestamp
@@ -376,7 +682,7 @@ async def get_suggested_blocks(count: int = 5) -> list[BlockInfo]:
# Sort blocks by execution count
blocks.sort(key=lambda x: x[1], reverse=True)
_suggested_blocks = [block[0] for block in blocks]
suggested_blocks = [block[0] for block in blocks]
# Return the top blocks
return _suggested_blocks[:count]
return suggested_blocks[:count]

View File

@@ -18,10 +18,17 @@ FilterType = Literal[
BlockType = Literal["all", "input", "action", "output"]
class SearchEntry(BaseModel):
search_query: str | None = None
filter: list[FilterType] | None = None
by_creator: list[str] | None = None
search_id: str | None = None
# Suggestions
class SuggestionsResponse(BaseModel):
otto_suggestions: list[str]
recent_searches: list[str]
recent_searches: list[SearchEntry]
providers: list[ProviderName]
top_blocks: list[BlockInfo]
@@ -32,7 +39,7 @@ class BlockCategoryResponse(BaseModel):
total_blocks: int
blocks: list[BlockInfo]
model_config = {"use_enum_values": False} # <== use enum names like "AI"
model_config = {"use_enum_values": False} # Use enum names like "AI"
# Input/Action/Output and see all for block categories
@@ -53,17 +60,11 @@ class ProviderResponse(BaseModel):
pagination: Pagination
class SearchBlocksResponse(BaseModel):
blocks: BlockResponse
total_block_count: int
total_integration_count: int
class SearchResponse(BaseModel):
items: list[BlockInfo | library_model.LibraryAgent | store_model.StoreAgent]
search_id: str
total_items: dict[FilterType, int]
page: int
more_pages: bool
pagination: Pagination
class CountResponse(BaseModel):

View File

@@ -6,10 +6,6 @@ from autogpt_libs.auth.dependencies import get_user_id, requires_user
import backend.server.v2.builder.db as builder_db
import backend.server.v2.builder.model as builder_model
import backend.server.v2.library.db as library_db
import backend.server.v2.library.model as library_model
import backend.server.v2.store.db as store_db
import backend.server.v2.store.model as store_model
from backend.integrations.providers import ProviderName
from backend.util.models import Pagination
@@ -45,7 +41,9 @@ def sanitize_query(query: str | None) -> str | None:
summary="Get Builder suggestions",
response_model=builder_model.SuggestionsResponse,
)
async def get_suggestions() -> builder_model.SuggestionsResponse:
async def get_suggestions(
user_id: Annotated[str, fastapi.Security(get_user_id)],
) -> builder_model.SuggestionsResponse:
"""
Get all suggestions for the Blocks Menu.
"""
@@ -55,11 +53,7 @@ async def get_suggestions() -> builder_model.SuggestionsResponse:
"Help me create a list",
"Help me feed my data to Google Maps",
],
recent_searches=[
"image generation",
"deepfake",
"competitor analysis",
],
recent_searches=await builder_db.get_recent_searches(user_id),
providers=[
ProviderName.TWITTER,
ProviderName.GITHUB,
@@ -147,7 +141,6 @@ async def get_providers(
)
# Not using post method because on frontend, orval doesn't support Infinite Query with POST method.
@router.get(
"/search",
summary="Builder search",
@@ -157,7 +150,7 @@ async def get_providers(
async def search(
user_id: Annotated[str, fastapi.Security(get_user_id)],
search_query: Annotated[str | None, fastapi.Query()] = None,
filter: Annotated[list[str] | None, fastapi.Query()] = None,
filter: Annotated[list[builder_model.FilterType] | None, fastapi.Query()] = None,
search_id: Annotated[str | None, fastapi.Query()] = None,
by_creator: Annotated[list[str] | None, fastapi.Query()] = None,
page: Annotated[int, fastapi.Query()] = 1,
@@ -176,69 +169,43 @@ async def search(
]
search_query = sanitize_query(search_query)
# Blocks&Integrations
blocks = builder_model.SearchBlocksResponse(
blocks=builder_model.BlockResponse(
blocks=[],
pagination=Pagination.empty(),
),
total_block_count=0,
total_integration_count=0,
# Get all possible results
cached_results = await builder_db.get_sorted_search_results(
user_id=user_id,
search_query=search_query,
filters=filter,
by_creator=by_creator,
)
if "blocks" in filter or "integrations" in filter:
blocks = builder_db.search_blocks(
include_blocks="blocks" in filter,
include_integrations="integrations" in filter,
query=search_query or "",
page=page,
page_size=page_size,
)
# Library Agents
my_agents = library_model.LibraryAgentResponse(
agents=[],
pagination=Pagination.empty(),
# Paginate results
total_combined_items = len(cached_results.items)
pagination = Pagination(
total_items=total_combined_items,
total_pages=(total_combined_items + page_size - 1) // page_size,
current_page=page,
page_size=page_size,
)
if "my_agents" in filter:
my_agents = await library_db.list_library_agents(
user_id=user_id,
search_term=search_query,
page=page,
page_size=page_size,
)
# Marketplace Agents
marketplace_agents = store_model.StoreAgentsResponse(
agents=[],
pagination=Pagination.empty(),
)
if "marketplace_agents" in filter:
marketplace_agents = await store_db.get_store_agents(
creators=by_creator,
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paginated_items = cached_results.items[start_idx:end_idx]
# Update the search entry by id
search_id = await builder_db.update_search(
user_id,
builder_model.SearchEntry(
search_query=search_query,
page=page,
page_size=page_size,
)
more_pages = False
if (
blocks.blocks.pagination.current_page < blocks.blocks.pagination.total_pages
or my_agents.pagination.current_page < my_agents.pagination.total_pages
or marketplace_agents.pagination.current_page
< marketplace_agents.pagination.total_pages
):
more_pages = True
filter=filter,
by_creator=by_creator,
search_id=search_id,
),
)
return builder_model.SearchResponse(
items=blocks.blocks.blocks + my_agents.agents + marketplace_agents.agents,
total_items={
"blocks": blocks.total_block_count,
"integrations": blocks.total_integration_count,
"marketplace_agents": marketplace_agents.pagination.total_items,
"my_agents": my_agents.pagination.total_items,
},
page=page,
more_pages=more_pages,
items=paginated_items,
search_id=search_id,
total_items=cached_results.total_items,
pagination=pagination,
)

View File

@@ -134,18 +134,14 @@ async def process_review_action(
# Build review decisions map
review_decisions = {}
for review in request.reviews:
if review.approved:
review_decisions[review.node_exec_id] = (
ReviewStatus.APPROVED,
review.reviewed_data,
review.message,
)
else:
review_decisions[review.node_exec_id] = (
ReviewStatus.REJECTED,
None,
review.message,
)
review_status = (
ReviewStatus.APPROVED if review.approved else ReviewStatus.REJECTED
)
review_decisions[review.node_exec_id] = (
review_status,
review.reviewed_data,
review.message,
)
# Process all reviews
updated_reviews = await process_all_reviews_for_execution(

View File

@@ -5,6 +5,13 @@ from tiktoken import encoding_for_model
from backend.util import json
# ---------------------------------------------------------------------------#
# CONSTANTS #
# ---------------------------------------------------------------------------#
# Message prefixes for important system messages that should be protected during compression
MAIN_OBJECTIVE_PREFIX = "[Main Objective Prompt]: "
# ---------------------------------------------------------------------------#
# INTERNAL UTILITIES #
# ---------------------------------------------------------------------------#
@@ -63,6 +70,55 @@ def _msg_tokens(msg: dict, enc) -> int:
return WRAPPER + content_tokens + tool_call_tokens
def _is_tool_message(msg: dict) -> bool:
"""Check if a message contains tool calls or results that should be protected."""
content = msg.get("content")
# Check for Anthropic-style tool messages
if isinstance(content, list) and any(
isinstance(item, dict) and item.get("type") in ("tool_use", "tool_result")
for item in content
):
return True
# Check for OpenAI-style tool calls in the message
if "tool_calls" in msg or msg.get("role") == "tool":
return True
return False
def _is_objective_message(msg: dict) -> bool:
"""Check if a message contains objective/system prompts that should be absolutely protected."""
content = msg.get("content", "")
if isinstance(content, str):
# Protect any message with the main objective prefix
return content.startswith(MAIN_OBJECTIVE_PREFIX)
return False
def _truncate_tool_message_content(msg: dict, enc, max_tokens: int) -> None:
"""
Carefully truncate tool message content while preserving tool structure.
Only truncates tool_result content, leaves tool_use intact.
"""
content = msg.get("content")
if not isinstance(content, list):
return
for item in content:
# Only process tool_result items, leave tool_use blocks completely intact
if not (isinstance(item, dict) and item.get("type") == "tool_result"):
continue
result_content = item.get("content", "")
if (
isinstance(result_content, str)
and _tok_len(result_content, enc) > max_tokens
):
item["content"] = _truncate_middle_tokens(result_content, enc, max_tokens)
def _truncate_middle_tokens(text: str, enc, max_tok: int) -> str:
"""
Return *text* shortened to ≈max_tok tokens by keeping the head & tail
@@ -140,13 +196,21 @@ def compress_prompt(
return sum(_msg_tokens(m, enc) for m in msgs)
original_token_count = total_tokens()
if original_token_count + reserve <= target_tokens:
return msgs
# ---- STEP 0 : normalise content --------------------------------------
# Convert non-string payloads to strings so token counting is coherent.
for m in msgs[1:-1]: # keep the first & last intact
for i, m in enumerate(msgs):
if not isinstance(m.get("content"), str) and m.get("content") is not None:
if _is_tool_message(m):
continue
# Keep first and last messages intact (unless they're tool messages)
if i == 0 or i == len(msgs) - 1:
continue
# Reasonable 20k-char ceiling prevents pathological blobs
content_str = json.dumps(m["content"], separators=(",", ":"))
if len(content_str) > 20_000:
@@ -157,34 +221,45 @@ def compress_prompt(
cap = start_cap
while total_tokens() + reserve > target_tokens and cap >= floor_cap:
for m in msgs[1:-1]: # keep first & last intact
if _tok_len(m.get("content") or "", enc) > cap:
m["content"] = _truncate_middle_tokens(m["content"], enc, cap)
if _is_tool_message(m):
# For tool messages, only truncate tool result content, preserve structure
_truncate_tool_message_content(m, enc, cap)
continue
if _is_objective_message(m):
# Never truncate objective messages - they contain the core task
continue
content = m.get("content") or ""
if _tok_len(content, enc) > cap:
m["content"] = _truncate_middle_tokens(content, enc, cap)
cap //= 2 # tighten the screw
# ---- STEP 2 : middle-out deletion -----------------------------------
while total_tokens() + reserve > target_tokens and len(msgs) > 2:
# Identify all deletable messages (not first/last, not tool messages, not objective messages)
deletable_indices = []
for i in range(1, len(msgs) - 1): # Skip first and last
if not _is_tool_message(msgs[i]) and not _is_objective_message(msgs[i]):
deletable_indices.append(i)
if not deletable_indices:
break # nothing more we can drop
# Delete from center outward - find the index closest to center
centre = len(msgs) // 2
# Build a symmetrical centre-out index walk: centre, centre+1, centre-1, ...
order = [centre] + [
i
for pair in zip(range(centre + 1, len(msgs) - 1), range(centre - 1, 0, -1))
for i in pair
]
removed = False
for i in order:
msg = msgs[i]
if "tool_calls" in msg or msg.get("role") == "tool":
continue # protect tool shells
del msgs[i]
removed = True
break
if not removed: # nothing more we can drop
break
to_delete = min(deletable_indices, key=lambda i: abs(i - centre))
del msgs[to_delete]
# ---- STEP 3 : final safety-net trim on first & last ------------------
cap = start_cap
while total_tokens() + reserve > target_tokens and cap >= floor_cap:
for idx in (0, -1): # first and last
if _is_tool_message(msgs[idx]):
# For tool messages at first/last position, truncate tool result content only
_truncate_tool_message_content(msgs[idx], enc, cap)
continue
text = msgs[idx].get("content") or ""
if _tok_len(text, enc) > cap:
msgs[idx]["content"] = _truncate_middle_tokens(text, enc, cap)

View File

@@ -0,0 +1,15 @@
-- Create BuilderSearchHistory table
CREATE TABLE "BuilderSearchHistory" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"searchQuery" TEXT NOT NULL,
"filter" TEXT[] DEFAULT ARRAY[]::TEXT[],
"byCreator" TEXT[] DEFAULT ARRAY[]::TEXT[],
CONSTRAINT "BuilderSearchHistory_pkey" PRIMARY KEY ("id")
);
-- Define User foreign relation
ALTER TABLE "BuilderSearchHistory" ADD CONSTRAINT "BuilderSearchHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -53,6 +53,7 @@ model User {
Profile Profile[]
UserOnboarding UserOnboarding?
BuilderSearchHistory BuilderSearchHistory[]
StoreListings StoreListing[]
StoreListingReviews StoreListingReview[]
StoreVersionsReviewed StoreListingVersion[]
@@ -114,6 +115,19 @@ model UserOnboarding {
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model BuilderSearchHistory {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
searchQuery String
filter String[] @default([])
byCreator String[] @default([])
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
// This model describes the Agent Graph/Flow (Multi Agent System).
model AgentGraph {
id String @default(uuid())

View File

@@ -3,6 +3,14 @@ import { withSentryConfig } from "@sentry/nextjs";
/** @type {import('next').NextConfig} */
const nextConfig = {
productionBrowserSourceMaps: true,
experimental: {
serverActions: {
bodySizeLimit: "256mb",
},
// Increase body size limit for API routes (file uploads) - 256MB to match backend limit
proxyClientMaxBodySize: "256mb",
middlewareClientMaxBodySize: "256mb",
},
images: {
domains: [
// We dont need to maintain alphabetical order here

View File

@@ -82,7 +82,7 @@
"lodash": "4.17.21",
"lucide-react": "0.552.0",
"moment": "2.30.1",
"next": "15.4.8",
"next": "15.4.10",
"next-themes": "0.4.6",
"nuqs": "2.7.2",
"party-js": "2.2.0",
@@ -137,9 +137,8 @@
"concurrently": "9.2.1",
"cross-env": "10.1.0",
"eslint": "8.57.1",
"eslint-config-next": "15.5.2",
"eslint-config-next": "15.5.7",
"eslint-plugin-storybook": "9.1.5",
"import-in-the-middle": "1.14.2",
"msw": "2.11.6",
"msw-storybook-addon": "2.0.6",
"orval": "7.13.0",

View File

@@ -16,7 +16,7 @@ importers:
version: 5.2.2(react-hook-form@7.66.0(react@18.3.1))
'@next/third-parties':
specifier: 15.4.6
version: 15.4.6(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.4.6(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@phosphor-icons/react':
specifier: 2.1.10
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -88,7 +88,7 @@ importers:
version: 5.24.13(@rjsf/utils@5.24.13(react@18.3.1))
'@sentry/nextjs':
specifier: 10.27.0
version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.101.3(esbuild@0.25.9))
version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.101.3(esbuild@0.25.9))
'@supabase/ssr':
specifier: 0.7.0
version: 0.7.0(@supabase/supabase-js@2.78.0)
@@ -106,10 +106,10 @@ importers:
version: 0.2.4
'@vercel/analytics':
specifier: 1.5.0
version: 1.5.0(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 1.5.0(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@vercel/speed-insights':
specifier: 1.2.0
version: 1.2.0(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 1.2.0(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@xyflow/react':
specifier: 12.9.2
version: 12.9.2(@types/react@18.3.17)(immer@10.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -148,7 +148,7 @@ importers:
version: 12.23.24(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
geist:
specifier: 1.5.1
version: 1.5.1(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
version: 1.5.1(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
highlight.js:
specifier: 11.11.1
version: 11.11.1
@@ -171,14 +171,14 @@ importers:
specifier: 2.30.1
version: 2.30.1
next:
specifier: 15.4.8
version: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: 15.4.10
version: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-themes:
specifier: 0.4.6
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nuqs:
specifier: 2.7.2
version: 2.7.2(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 2.7.2(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
party-js:
specifier: 2.2.0
version: 2.2.0
@@ -284,7 +284,7 @@ importers:
version: 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))
'@storybook/nextjs':
specifier: 9.1.5
version: 9.1.5(esbuild@0.25.9)(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.101.3(esbuild@0.25.9))
version: 9.1.5(esbuild@0.25.9)(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.101.3(esbuild@0.25.9))
'@tanstack/eslint-plugin-query':
specifier: 5.91.2
version: 5.91.2(eslint@8.57.1)(typescript@5.9.3)
@@ -331,14 +331,11 @@ importers:
specifier: 8.57.1
version: 8.57.1
eslint-config-next:
specifier: 15.5.2
version: 15.5.2(eslint@8.57.1)(typescript@5.9.3)
specifier: 15.5.7
version: 15.5.7(eslint@8.57.1)(typescript@5.9.3)
eslint-plugin-storybook:
specifier: 9.1.5
version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)
import-in-the-middle:
specifier: 1.14.2
version: 1.14.2
msw:
specifier: 2.11.6
version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3)
@@ -986,12 +983,15 @@ packages:
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -1329,6 +1329,10 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/eslintrc@2.1.4':
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1602,11 +1606,11 @@ packages:
'@neoconfetti/react@1.0.0':
resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==}
'@next/env@15.4.8':
resolution: {integrity: sha512-LydLa2MDI1NMrOFSkO54mTc8iIHSttj6R6dthITky9ylXV2gCGi0bHQjVCtLGRshdRPjyh2kXbxJukDtBWQZtQ==}
'@next/env@15.4.10':
resolution: {integrity: sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg==}
'@next/eslint-plugin-next@15.5.2':
resolution: {integrity: sha512-lkLrRVxcftuOsJNhWatf1P2hNVfh98k/omQHrCEPPriUypR6RcS13IvLdIrEvkm9AH2Nu2YpR5vLqBuy6twH3Q==}
'@next/eslint-plugin-next@15.5.7':
resolution: {integrity: sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==}
'@next/swc-darwin-arm64@15.4.8':
resolution: {integrity: sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==}
@@ -2622,8 +2626,8 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@rushstack/eslint-patch@1.12.0':
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
'@rushstack/eslint-patch@1.15.0':
resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==}
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
@@ -3097,8 +3101,8 @@ packages:
peerDependencies:
'@testing-library/dom': '>=7.21.4'
'@tybys/wasm-util@0.10.0':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -3288,16 +3292,16 @@ packages:
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typescript-eslint/eslint-plugin@8.43.0':
resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==}
'@typescript-eslint/eslint-plugin@8.48.1':
resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.43.0
'@typescript-eslint/parser': ^8.48.1
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.43.0':
resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==}
'@typescript-eslint/parser@8.48.1':
resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -3315,6 +3319,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.48.1':
resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.43.0':
resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3323,6 +3333,10 @@ packages:
resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.48.1':
resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.43.0':
resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3335,8 +3349,14 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.43.0':
resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==}
'@typescript-eslint/tsconfig-utils@8.48.1':
resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.48.1':
resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -3350,6 +3370,10 @@ packages:
resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.48.1':
resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.43.0':
resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3362,6 +3386,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/typescript-estree@8.48.1':
resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.43.0':
resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3376,6 +3406,13 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.48.1':
resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.43.0':
resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3384,6 +3421,10 @@ packages:
resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.48.1':
resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -4585,8 +4626,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-config-next@15.5.2:
resolution: {integrity: sha512-3hPZghsLupMxxZ2ggjIIrat/bPniM2yRpsVPVM40rp8ZMzKWOJp2CGWn7+EzoV2ddkUr5fxNfHpF+wU1hGt/3g==}
eslint-config-next@15.5.7:
resolution: {integrity: sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@@ -4918,6 +4959,10 @@ packages:
peerDependencies:
next: '>=13.2.0'
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -4946,8 +4991,8 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
@@ -5168,9 +5213,6 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
import-in-the-middle@1.14.2:
resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==}
import-in-the-middle@2.0.0:
resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==}
@@ -5282,6 +5324,10 @@ packages:
resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
engines: {node: '>= 0.4'}
is-generator-function@1.1.2:
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
engines: {node: '>= 0.4'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -5903,8 +5949,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-postinstall@0.3.3:
resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==}
napi-postinstall@0.3.4:
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
hasBin: true
@@ -5920,8 +5966,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@15.4.8:
resolution: {integrity: sha512-jwOXTz/bo0Pvlf20FSb6VXVeWRssA2vbvq9SdrOPEg9x8E1B27C2rQtvriAn600o9hH61kjrVRexEffv3JybuA==}
next@15.4.10:
resolution: {integrity: sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -6769,6 +6815,11 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
resolve@1.22.11:
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
engines: {node: '>= 0.4'}
hasBin: true
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
@@ -7858,7 +7909,7 @@ snapshots:
'@babel/helper-plugin-utils': 7.27.1
debug: 4.4.3
lodash.debounce: 4.0.8
resolve: 1.22.10
resolve: 1.22.11
transitivePeerDependencies:
- supports-color
@@ -8550,7 +8601,7 @@ snapshots:
'@date-fns/tz@1.4.1': {}
'@emnapi/core@1.5.0':
'@emnapi/core@1.7.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
@@ -8561,6 +8612,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.7.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
@@ -8739,6 +8795,8 @@ snapshots:
'@eslint-community/regexpp@4.12.1': {}
'@eslint-community/regexpp@4.12.2': {}
'@eslint/eslintrc@2.1.4':
dependencies:
ajv: 6.12.6
@@ -8996,16 +9054,16 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
'@tybys/wasm-util': 0.10.0
'@emnapi/core': 1.7.1
'@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.10.1
optional: true
'@neoconfetti/react@1.0.0': {}
'@next/env@15.4.8': {}
'@next/env@15.4.10': {}
'@next/eslint-plugin-next@15.5.2':
'@next/eslint-plugin-next@15.5.7':
dependencies:
fast-glob: 3.3.1
@@ -9033,9 +9091,9 @@ snapshots:
'@next/swc-win32-x64-msvc@15.4.8':
optional: true
'@next/third-parties@15.4.6(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
'@next/third-parties@15.4.6(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
third-party-capital: 1.0.20
@@ -10115,7 +10173,7 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.12.0': {}
'@rushstack/eslint-patch@1.15.0': {}
'@scarf/scarf@1.4.0': {}
@@ -10267,7 +10325,7 @@ snapshots:
'@sentry/core@10.27.0': {}
'@sentry/nextjs@10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.101.3(esbuild@0.25.9))':
'@sentry/nextjs@10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.101.3(esbuild@0.25.9))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.37.0
@@ -10280,7 +10338,7 @@ snapshots:
'@sentry/react': 10.27.0(react@18.3.1)
'@sentry/vercel-edge': 10.27.0
'@sentry/webpack-plugin': 4.3.0(webpack@5.101.3(esbuild@0.25.9))
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
resolve: 1.22.8
rollup: 4.52.2
stacktrace-parser: 0.1.11
@@ -10642,7 +10700,7 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@storybook/nextjs@9.1.5(esbuild@0.25.9)(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.101.3(esbuild@0.25.9))':
'@storybook/nextjs@9.1.5(esbuild@0.25.9)(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.101.3(esbuild@0.25.9))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4)
@@ -10666,7 +10724,7 @@ snapshots:
css-loader: 6.11.0(webpack@5.101.3(esbuild@0.25.9))
image-size: 2.0.2
loader-utils: 3.3.1
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
node-polyfill-webpack-plugin: 2.0.1(webpack@5.101.3(esbuild@0.25.9))
postcss: 8.5.6
postcss-loader: 8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.101.3(esbuild@0.25.9))
@@ -10867,7 +10925,7 @@ snapshots:
dependencies:
'@testing-library/dom': 10.4.1
'@tybys/wasm-util@0.10.0':
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -11065,14 +11123,14 @@ snapshots:
dependencies:
'@types/node': 24.10.0
'@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.43.0
'@typescript-eslint/type-utils': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.43.0
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.48.1
'@typescript-eslint/type-utils': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.1
eslint: 8.57.1
graphemer: 1.4.0
ignore: 7.0.5
@@ -11082,12 +11140,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.43.0
'@typescript-eslint/types': 8.43.0
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.43.0
'@typescript-eslint/scope-manager': 8.48.1
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.1
debug: 4.4.3
eslint: 8.57.1
typescript: 5.9.3
@@ -11097,7 +11155,7 @@ snapshots:
'@typescript-eslint/project-service@8.43.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.3)
'@typescript-eslint/types': 8.43.0
'@typescript-eslint/types': 8.48.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
@@ -11106,7 +11164,16 @@ snapshots:
'@typescript-eslint/project-service@8.46.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/types': 8.48.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.48.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
'@typescript-eslint/types': 8.48.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
@@ -11122,6 +11189,11 @@ snapshots:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2
'@typescript-eslint/scope-manager@8.48.1':
dependencies:
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/visitor-keys': 8.48.1
'@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
@@ -11130,11 +11202,15 @@ snapshots:
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.43.0(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.43.0
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
typescript: 5.9.3
'@typescript-eslint/type-utils@8.48.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.3
eslint: 8.57.1
ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -11146,6 +11222,8 @@ snapshots:
'@typescript-eslint/types@8.46.2': {}
'@typescript-eslint/types@8.48.1': {}
'@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.43.0(typescript@5.9.3)
@@ -11156,7 +11234,7 @@ snapshots:
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.2
semver: 7.7.3
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
@@ -11178,6 +11256,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.48.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/visitor-keys': 8.48.1
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
@@ -11200,6 +11293,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.48.1
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.43.0':
dependencies:
'@typescript-eslint/types': 8.43.0
@@ -11210,6 +11314,11 @@ snapshots:
'@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1
'@typescript-eslint/visitor-keys@8.48.1':
dependencies:
'@typescript-eslint/types': 8.48.1
eslint-visitor-keys: 4.2.1
'@ungap/structured-clone@1.3.0': {}
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -11271,14 +11380,14 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vercel/analytics@1.5.0(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
'@vercel/analytics@1.5.0(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
optionalDependencies:
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
'@vercel/speed-insights@1.2.0(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
'@vercel/speed-insights@1.2.0(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
optionalDependencies:
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
'@vitest/expect@3.2.4':
@@ -12532,16 +12641,16 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@15.5.2(eslint@8.57.1)(typescript@5.9.3):
eslint-config-next@15.5.7(eslint@8.57.1)(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 15.5.2
'@rushstack/eslint-patch': 1.12.0
'@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
'@next/eslint-plugin-next': 15.5.7
'@rushstack/eslint-patch': 1.15.0
'@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@@ -12556,7 +12665,7 @@ snapshots:
dependencies:
debug: 3.2.7
is-core-module: 2.16.1
resolve: 1.22.10
resolve: 1.22.11
transitivePeerDependencies:
- supports-color
@@ -12565,28 +12674,28 @@ snapshots:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
eslint: 8.57.1
get-tsconfig: 4.10.1
get-tsconfig: 4.13.0
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -12597,7 +12706,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -12609,7 +12718,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -12954,9 +13063,11 @@ snapshots:
functions-have-names@1.2.3: {}
geist@1.5.1(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
geist@1.5.1(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies:
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}
@@ -12990,7 +13101,7 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
get-tsconfig@4.10.1:
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -13274,13 +13385,6 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
import-in-the-middle@1.14.2:
dependencies:
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
cjs-module-lexer: 1.4.3
module-details-from-path: 1.0.4
import-in-the-middle@2.0.0:
dependencies:
acorn: 8.15.0
@@ -13357,7 +13461,7 @@ snapshots:
is-bun-module@2.0.0:
dependencies:
semver: 7.7.2
semver: 7.7.3
is-callable@1.2.7: {}
@@ -13395,6 +13499,14 @@ snapshots:
has-tostringtag: 1.0.2
safe-regex-test: 1.1.0
is-generator-function@1.1.2:
dependencies:
call-bound: 1.0.4
generator-function: 2.0.1
get-proto: 1.0.1
has-tostringtag: 1.0.2
safe-regex-test: 1.1.0
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
@@ -14215,7 +14327,7 @@ snapshots:
nanoid@3.3.11: {}
napi-postinstall@0.3.3: {}
napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {}
@@ -14226,9 +14338,9 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 15.4.8
'@next/env': 15.4.10
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001741
postcss: 8.4.31
@@ -14321,12 +14433,12 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.7.2(next@15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
nuqs@2.7.2(next@15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
'@standard-schema/spec': 1.0.0
react: 18.3.1
optionalDependencies:
next: 15.4.8(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
oas-kit-common@1.0.8:
dependencies:
@@ -15185,6 +15297,12 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
resolve@1.22.11:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
resolve@1.22.8:
dependencies:
is-core-module: 2.16.1
@@ -15996,7 +16114,7 @@ snapshots:
unrs-resolver@1.11.1:
dependencies:
napi-postinstall: 0.3.3
napi-postinstall: 0.3.4
optionalDependencies:
'@unrs/resolver-binding-android-arm-eabi': 1.11.1
'@unrs/resolver-binding-android-arm64': 1.11.1
@@ -16224,7 +16342,7 @@ snapshots:
is-async-function: 2.1.1
is-date-object: 1.1.0
is-finalizationregistry: 1.1.1
is-generator-function: 1.1.0
is-generator-function: 1.1.2
is-regex: 1.2.1
is-weakref: 1.1.1
isarray: 2.0.5

View File

@@ -8,7 +8,6 @@ import {
CardTitle,
} from "@/components/__legacy__/ui/card";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import { Play } from "lucide-react";
import OnboardingButton from "../components/OnboardingButton";
@@ -79,20 +78,13 @@ export default function Page() {
<CardContent className="flex flex-col gap-4">
{Object.entries(agent?.input_schema.properties || {}).map(
([key, inputSubSchema]) => (
<div key={key} className="flex flex-col space-y-2">
<label className="flex items-center gap-1 text-sm font-medium">
{inputSubSchema.title || key}
<InformationTooltip
description={inputSubSchema.description}
/>
</label>
<RunAgentInputs
schema={inputSubSchema}
value={onboarding.state?.agentInput?.[key]}
placeholder={inputSubSchema.description}
onChange={(value) => handleSetAgentInput(key, value)}
/>
</div>
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={onboarding.state?.agentInput?.[key]}
placeholder={inputSubSchema.description}
onChange={(value) => handleSetAgentInput(key, value)}
/>
),
)}
<AgentOnboardingCredentials

View File

@@ -1,24 +1,25 @@
import { useCallback } from "react";
import { useReactFlow } from "@xyflow/react";
import { Key, storage } from "@/services/storage/local-storage";
import { v4 as uuidv4 } from "uuid";
import { useNodeStore } from "../../../stores/nodeStore";
import { useEdgeStore } from "../../../stores/edgeStore";
import { CustomNode } from "../nodes/CustomNode/CustomNode";
import { CustomEdge } from "../edges/CustomEdge";
import { useToast } from "@/components/molecules/Toast/use-toast";
interface CopyableData {
nodes: CustomNode[];
edges: CustomEdge[];
}
const CLIPBOARD_PREFIX = "autogpt-flow-data:";
export function useCopyPaste() {
// Only use useReactFlow for viewport (not managed by stores)
const { getViewport } = useReactFlow();
const { toast } = useToast();
const handleCopyPaste = useCallback(
(event: KeyboardEvent) => {
// Prevent copy/paste if any modal is open or if the focus is on an input element
const activeElement = document.activeElement;
const isInputField =
activeElement?.tagName === "INPUT" ||
@@ -28,7 +29,6 @@ export function useCopyPaste() {
if (isInputField) return;
if (event.ctrlKey || event.metaKey) {
// COPY: Ctrl+C or Cmd+C
if (event.key === "c" || event.key === "C") {
const { nodes } = useNodeStore.getState();
const { edges } = useEdgeStore.getState();
@@ -53,81 +53,102 @@ export function useCopyPaste() {
edges: selectedEdges,
};
storage.set(Key.COPIED_FLOW_DATA, JSON.stringify(copiedData));
const clipboardText = `${CLIPBOARD_PREFIX}${JSON.stringify(copiedData)}`;
navigator.clipboard
.writeText(clipboardText)
.then(() => {
toast({
title: "Copied successfully",
description: `${selectedNodes.length} node(s) copied to clipboard`,
});
})
.catch((error) => {
console.error("Failed to copy to clipboard:", error);
});
}
// PASTE: Ctrl+V or Cmd+V
if (event.key === "v" || event.key === "V") {
const copiedDataString = storage.get(Key.COPIED_FLOW_DATA);
if (copiedDataString) {
const copiedData = JSON.parse(copiedDataString) as CopyableData;
const oldToNewIdMap: Record<string, string> = {};
navigator.clipboard
.readText()
.then((clipboardText) => {
if (!clipboardText.startsWith(CLIPBOARD_PREFIX)) {
return; // Not our data, ignore
}
// Get fresh viewport values at paste time to ensure correct positioning
const { x, y, zoom } = getViewport();
const viewportCenter = {
x: (window.innerWidth / 2 - x) / zoom,
y: (window.innerHeight / 2 - y) / zoom,
};
const jsonString = clipboardText.slice(CLIPBOARD_PREFIX.length);
const copiedData = JSON.parse(jsonString) as CopyableData;
const oldToNewIdMap: Record<string, string> = {};
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
copiedData.nodes.forEach((node) => {
minX = Math.min(minX, node.position.x);
minY = Math.min(minY, node.position.y);
maxX = Math.max(maxX, node.position.x);
maxY = Math.max(maxY, node.position.y);
});
const offsetX = viewportCenter.x - (minX + maxX) / 2;
const offsetY = viewportCenter.y - (minY + maxY) / 2;
// Deselect existing nodes first
useNodeStore.setState((state) => ({
nodes: state.nodes.map((node) => ({ ...node, selected: false })),
}));
// Create and add new nodes with UNIQUE IDs using UUID
copiedData.nodes.forEach((node) => {
const newNodeId = uuidv4();
oldToNewIdMap[node.id] = newNodeId;
const newNode: CustomNode = {
...node,
id: newNodeId,
selected: true,
position: {
x: node.position.x + offsetX,
y: node.position.y + offsetY,
},
const { x, y, zoom } = getViewport();
const viewportCenter = {
x: (window.innerWidth / 2 - x) / zoom,
y: (window.innerHeight / 2 - y) / zoom,
};
useNodeStore.getState().addNode(newNode);
});
// Add edges with updated source/target IDs
const { addEdge } = useEdgeStore.getState();
copiedData.edges.forEach((edge) => {
const newSourceId = oldToNewIdMap[edge.source] ?? edge.source;
const newTargetId = oldToNewIdMap[edge.target] ?? edge.target;
addEdge({
source: newSourceId,
target: newTargetId,
sourceHandle: edge.sourceHandle ?? "",
targetHandle: edge.targetHandle ?? "",
data: {
...edge.data,
},
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
copiedData.nodes.forEach((node) => {
minX = Math.min(minX, node.position.x);
minY = Math.min(minY, node.position.y);
maxX = Math.max(maxX, node.position.x);
maxY = Math.max(maxY, node.position.y);
});
const offsetX = viewportCenter.x - (minX + maxX) / 2;
const offsetY = viewportCenter.y - (minY + maxY) / 2;
// Deselect existing nodes first
useNodeStore.setState((state) => ({
nodes: state.nodes.map((node) => ({
...node,
selected: false,
})),
}));
// Create and add new nodes with UNIQUE IDs using UUID
copiedData.nodes.forEach((node) => {
const newNodeId = uuidv4();
oldToNewIdMap[node.id] = newNodeId;
const newNode: CustomNode = {
...node,
id: newNodeId,
selected: true,
position: {
x: node.position.x + offsetX,
y: node.position.y + offsetY,
},
};
useNodeStore.getState().addNode(newNode);
});
// Add edges with updated source/target IDs
const { addEdge } = useEdgeStore.getState();
copiedData.edges.forEach((edge) => {
const newSourceId = oldToNewIdMap[edge.source] ?? edge.source;
const newTargetId = oldToNewIdMap[edge.target] ?? edge.target;
addEdge({
source: newSourceId,
target: newTargetId,
sourceHandle: edge.sourceHandle ?? "",
targetHandle: edge.targetHandle ?? "",
data: {
...edge.data,
},
});
});
})
.catch((error) => {
console.error("Failed to read from clipboard:", error);
});
}
}
}
},
[getViewport],
[getViewport, toast],
);
return handleCopyPaste;

View File

@@ -42,11 +42,12 @@ export const useFlow = () => {
const setBlockMenuOpen = useControlPanelStore(
useShallow((state) => state.setBlockMenuOpen),
);
const [{ flowID, flowVersion, flowExecutionID }] = useQueryStates({
flowID: parseAsString,
flowVersion: parseAsInteger,
flowExecutionID: parseAsString,
});
const [{ flowID, flowVersion, flowExecutionID }, setQueryStates] =
useQueryStates({
flowID: parseAsString,
flowVersion: parseAsInteger,
flowExecutionID: parseAsString,
});
const { data: executionDetails } = useGetV1GetExecutionDetails(
flowID || "",
@@ -102,6 +103,9 @@ export const useFlow = () => {
// load graph schemas
useEffect(() => {
if (graph) {
setQueryStates({
flowVersion: graph.version ?? 1,
});
setGraphSchemas(
graph.input_schema as Record<string, any> | null,
graph.credentials_input_schema as Record<string, any> | null,

View File

@@ -106,7 +106,11 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
/>
<NodeAdvancedToggle nodeId={nodeId} />
{data.uiType != BlockUIType.OUTPUT && (
<OutputHandler outputSchema={outputSchema} nodeId={nodeId} />
<OutputHandler
uiType={data.uiType}
outputSchema={outputSchema}
nodeId={nodeId}
/>
)}
<NodeDataRenderer nodeId={nodeId} />
</div>

View File

@@ -20,17 +20,32 @@ export const FormCreator = React.memo(
className?: string;
}) => {
const updateNodeData = useNodeStore((state) => state.updateNodeData);
const getHardCodedValues = useNodeStore(
(state) => state.getHardCodedValues,
);
const handleChange = ({ formData }: any) => {
if ("credentials" in formData && !formData.credentials?.id) {
delete formData.credentials;
}
updateNodeData(nodeId, { hardcodedValues: formData });
const updatedValues =
uiType === BlockUIType.AGENT
? {
...getHardCodedValues(nodeId),
inputs: formData,
}
: formData;
updateNodeData(nodeId, { hardcodedValues: updatedValues });
};
const initialValues = getHardCodedValues(nodeId);
const hardcodedValues = getHardCodedValues(nodeId);
const initialValues =
uiType === BlockUIType.AGENT
? (hardcodedValues.inputs ?? {})
: hardcodedValues;
return (
<div className={className}>

View File

@@ -14,13 +14,16 @@ import {
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { getTypeDisplayInfo } from "./helpers";
import { generateHandleId } from "../handlers/helpers";
import { BlockUIType } from "../../types";
export const OutputHandler = ({
outputSchema,
nodeId,
uiType,
}: {
outputSchema: RJSFSchema;
nodeId: string;
uiType: BlockUIType;
}) => {
const { isOutputConnected } = useEdgeStore();
const properties = outputSchema?.properties || {};
@@ -79,7 +82,9 @@ export const OutputHandler = ({
</Text>
<NodeHandle
handleId={generateHandleId(key)}
handleId={
uiType === BlockUIType.AGENT ? key : generateHandleId(key)
}
isConnected={isConnected}
side="right"
/>

View File

@@ -1,24 +1,36 @@
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
import { useGetV2BuilderSearchInfinite } from "@/app/api/__generated__/endpoints/store/store";
import { SearchResponse } from "@/app/api/__generated__/models/searchResponse";
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useAddAgentToBuilder } from "../hooks/useAddAgentToBuilder";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { getV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import {
getGetV2ListLibraryAgentsQueryKey,
getV2GetLibraryAgent,
usePostV2AddMarketplaceAgent,
} from "@/app/api/__generated__/endpoints/library/library";
import { getGetV2GetBuilderItemCountsQueryKey } from "@/app/api/__generated__/endpoints/default/default";
import {
getGetV2GetBuilderItemCountsQueryKey,
getGetV2GetBuilderSuggestionsQueryKey,
} from "@/app/api/__generated__/endpoints/default/default";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { useToast } from "@/components/molecules/Toast/use-toast";
import * as Sentry from "@sentry/nextjs";
export const useBlockMenuSearch = () => {
const { searchQuery } = useBlockMenuStore();
const { searchQuery, searchId, setSearchId } = useBlockMenuStore();
const { toast } = useToast();
const { addAgentToBuilder, addLibraryAgentToBuilder } =
useAddAgentToBuilder();
const queryClient = getQueryClient();
const resetSearchSession = useCallback(() => {
setSearchId(undefined);
queryClient.invalidateQueries({
queryKey: getGetV2GetBuilderSuggestionsQueryKey(),
});
}, [queryClient, setSearchId]);
const [addingLibraryAgentId, setAddingLibraryAgentId] = useState<
string | null
@@ -38,13 +50,19 @@ export const useBlockMenuSearch = () => {
page: 1,
page_size: 8,
search_query: searchQuery,
search_id: searchId,
},
{
query: {
getNextPageParam: (lastPage, allPages) => {
const pagination = lastPage.data as SearchResponse;
const isMore = pagination.more_pages;
return isMore ? allPages.length + 1 : undefined;
getNextPageParam: (lastPage) => {
const response = lastPage.data as SearchResponse;
const { pagination } = response;
if (!pagination) {
return undefined;
}
const { current_page, total_pages } = pagination;
return current_page < total_pages ? current_page + 1 : undefined;
},
},
},
@@ -53,7 +71,6 @@ export const useBlockMenuSearch = () => {
const { mutateAsync: addMarketplaceAgent } = usePostV2AddMarketplaceAgent({
mutation: {
onSuccess: () => {
const queryClient = getQueryClient();
queryClient.invalidateQueries({
queryKey: getGetV2ListLibraryAgentsQueryKey(),
});
@@ -75,6 +92,24 @@ export const useBlockMenuSearch = () => {
},
});
useEffect(() => {
if (!searchData?.pages?.length) {
return;
}
const latestPage = searchData.pages[searchData.pages.length - 1];
const response = latestPage?.data as SearchResponse;
if (response?.search_id && response.search_id !== searchId) {
setSearchId(response.search_id);
}
}, [searchData, searchId, setSearchId]);
useEffect(() => {
if (searchId && !searchQuery) {
resetSearchSession();
}
}, [resetSearchSession, searchId, searchQuery]);
const allSearchData =
searchData?.pages?.flatMap((page) => {
const response = page.data as SearchResponse;
@@ -117,7 +152,12 @@ export const useBlockMenuSearch = () => {
});
const libraryAgent = response.data as LibraryAgent;
addAgentToBuilder(libraryAgent);
const { data: libraryAgentDetails } = await getV2GetLibraryAgent(
libraryAgent.id,
);
addAgentToBuilder(libraryAgentDetails as LibraryAgent);
toast({
title: "Agent Added",

View File

@@ -1,30 +1,32 @@
import { debounce } from "lodash";
import { useCallback, useEffect, useRef, useState } from "react";
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { getGetV2GetBuilderSuggestionsQueryKey } from "@/app/api/__generated__/endpoints/default/default";
const SEARCH_DEBOUNCE_MS = 300;
export const useBlockMenuSearchBar = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [localQuery, setLocalQuery] = useState("");
const { setSearchQuery, setSearchId, searchId, searchQuery } =
useBlockMenuStore();
const { setSearchQuery, setSearchId, searchQuery } = useBlockMenuStore();
const queryClient = getQueryClient();
const searchIdRef = useRef(searchId);
useEffect(() => {
searchIdRef.current = searchId;
}, [searchId]);
const clearSearchSession = useCallback(() => {
setSearchId(undefined);
queryClient.invalidateQueries({
queryKey: getGetV2GetBuilderSuggestionsQueryKey(),
});
}, [queryClient, setSearchId]);
const debouncedSetSearchQuery = useCallback(
debounce((value: string) => {
setSearchQuery(value);
if (value.length === 0) {
setSearchId(undefined);
} else if (!searchIdRef.current) {
setSearchId(crypto.randomUUID());
clearSearchSession();
}
}, SEARCH_DEBOUNCE_MS),
[setSearchQuery, setSearchId],
[clearSearchSession, setSearchQuery],
);
useEffect(() => {
@@ -36,13 +38,13 @@ export const useBlockMenuSearchBar = () => {
const handleClear = () => {
setLocalQuery("");
setSearchQuery("");
setSearchId(undefined);
clearSearchSession();
debouncedSetSearchQuery.cancel();
};
useEffect(() => {
setLocalQuery(searchQuery);
}, []);
}, [searchQuery]);
return {
handleClear,

View File

@@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState } from "react";
import { ArrowLeftIcon, ArrowRightIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
interface HorizontalScrollAreaProps {
children: React.ReactNode;
wrapperClassName?: string;
scrollContainerClassName?: string;
scrollAmount?: number;
dependencyList?: React.DependencyList;
}
const defaultDependencies: React.DependencyList = [];
const baseScrollClasses =
"flex gap-2 overflow-x-auto px-8 [scrollbar-width:none] [-ms-overflow-style:'none'] [&::-webkit-scrollbar]:hidden";
export const HorizontalScroll: React.FC<HorizontalScrollAreaProps> = ({
children,
wrapperClassName,
scrollContainerClassName,
scrollAmount = 300,
dependencyList = defaultDependencies,
}) => {
const scrollRef = useRef<HTMLDivElement | null>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const scrollByDelta = (delta: number) => {
if (!scrollRef.current) {
return;
}
scrollRef.current.scrollBy({ left: delta, behavior: "smooth" });
};
const updateScrollState = () => {
const element = scrollRef.current;
if (!element) {
setCanScrollLeft(false);
setCanScrollRight(false);
return;
}
setCanScrollLeft(element.scrollLeft > 0);
setCanScrollRight(
Math.ceil(element.scrollLeft + element.clientWidth) < element.scrollWidth,
);
};
useEffect(() => {
updateScrollState();
const element = scrollRef.current;
if (!element) {
return;
}
const handleScroll = () => updateScrollState();
element.addEventListener("scroll", handleScroll);
window.addEventListener("resize", handleScroll);
return () => {
element.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleScroll);
};
}, dependencyList);
return (
<div className={wrapperClassName}>
<div className="group relative">
<div
ref={scrollRef}
className={cn(baseScrollClasses, scrollContainerClassName)}
>
{children}
</div>
{canScrollLeft && (
<div className="pointer-events-none absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-white via-white/80 to-white/0" />
)}
{canScrollRight && (
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-white via-white/80 to-white/0" />
)}
{canScrollLeft && (
<button
type="button"
aria-label="Scroll left"
className="pointer-events-none absolute left-2 top-5 -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
onClick={() => scrollByDelta(-scrollAmount)}
>
<ArrowLeftIcon
size={28}
className="rounded-full bg-zinc-700 p-1 text-white drop-shadow"
weight="light"
/>
</button>
)}
{canScrollRight && (
<button
type="button"
aria-label="Scroll right"
className="pointer-events-none absolute right-2 top-5 -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
onClick={() => scrollByDelta(scrollAmount)}
>
<ArrowRightIcon
size={28}
className="rounded-full bg-zinc-700 p-1 text-white drop-shadow"
weight="light"
/>
</button>
)}
</div>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import { getGetV2GetBuilderItemCountsQueryKey } from "@/app/api/__generated__/endpoints/default/default";
import {
getGetV2ListLibraryAgentsQueryKey,
getV2GetLibraryAgent,
usePostV2AddMarketplaceAgent,
} from "@/app/api/__generated__/endpoints/library/library";
import {
@@ -105,8 +106,16 @@ export const useMarketplaceAgentsContent = () => {
},
});
// Here, libraryAgent has empty input and output schemas.
// Not updating the endpoint because this endpoint is used elsewhere.
// TODO: Create a new endpoint for builder specific to marketplace agents.
const libraryAgent = response.data as LibraryAgent;
addAgentToBuilder(libraryAgent);
const { data: libraryAgentDetails } = await getV2GetLibraryAgent(
libraryAgent.id,
);
addAgentToBuilder(libraryAgentDetails as LibraryAgent);
toast({
title: "Agent Added",

View File

@@ -6,10 +6,15 @@ import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { blockMenuContainerStyle } from "../style";
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
import { DefaultStateType } from "../types";
import { SearchHistoryChip } from "../SearchHistoryChip";
import { HorizontalScroll } from "../HorizontalScroll";
export const SuggestionContent = () => {
const { setIntegration, setDefaultState } = useBlockMenuStore();
const { setIntegration, setDefaultState, setSearchQuery, setSearchId } =
useBlockMenuStore();
const { data, isLoading, isError, error, refetch } = useSuggestionContent();
const suggestions = data?.suggestions;
const hasRecentSearches = (suggestions?.recent_searches?.length ?? 0) > 0;
if (isError) {
return (
@@ -29,11 +34,45 @@ export const SuggestionContent = () => {
);
}
const suggestions = data?.suggestions;
return (
<div className={blockMenuContainerStyle}>
<div className="w-full space-y-6 pb-4">
{/* Recent searches */}
{hasRecentSearches && (
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Recent searches
</p>
<HorizontalScroll
wrapperClassName="-mx-8"
scrollContainerClassName="flex gap-2 overflow-x-auto px-8 [scrollbar-width:none] [-ms-overflow-style:'none'] [&::-webkit-scrollbar]:hidden"
dependencyList={[
suggestions?.recent_searches?.length ?? 0,
isLoading,
]}
>
{!isLoading && suggestions
? suggestions.recent_searches.map((entry, index) => (
<SearchHistoryChip
key={entry.search_id || `${entry.search_query}-${index}`}
content={entry.search_query || "Untitled search"}
onClick={() => {
setSearchQuery(entry.search_query || "");
setSearchId(entry.search_id || undefined);
}}
/>
))
: Array(3)
.fill(0)
.map((_, index) => (
<SearchHistoryChip.Skeleton
key={`recent-search-skeleton-${index}`}
/>
))}
</HorizontalScroll>
</div>
)}
{/* Integrations */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">

View File

@@ -10,10 +10,13 @@ import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
import { EmptySchedules } from "./components/other/EmptySchedules";
import { EmptyTasks } from "./components/other/EmptyTasks";
import { EmptyTemplates } from "./components/other/EmptyTemplates";
import { EmptyTriggers } from "./components/other/EmptyTriggers";
import { SectionWrap } from "./components/other/SectionWrap";
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
@@ -21,11 +24,13 @@ import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
export function NewAgentLibraryView() {
const {
agent,
hasAnyItems,
ready,
error,
agentId,
agent,
ready,
activeTemplate,
isTemplateLoading,
error,
hasAnyItems,
activeItem,
sidebarLoading,
activeTab,
@@ -33,6 +38,9 @@ export function NewAgentLibraryView() {
handleSelectRun,
handleCountsChange,
handleClearSelectedRun,
onRunInitiated,
onTriggerSetup,
onScheduleCreated,
} = useNewAgentLibraryView();
if (error) {
@@ -62,14 +70,19 @@ export function NewAgentLibraryView() {
/>
</div>
<div className="flex min-h-0 flex-1">
<EmptyTasks agent={agent} />
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</div>
</div>
);
}
return (
<div className="ml-4 grid h-full grid-cols-1 gap-0 pt-3 md:gap-4 lg:grid-cols-[25%_70%]">
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
<SectionWrap className="mb-3 block">
<div
className={cn(
@@ -79,16 +92,21 @@ export function NewAgentLibraryView() {
>
<RunAgentModal
triggerSlot={
<Button variant="primary" size="large" className="w-full">
<Button
variant="primary"
size="large"
className="w-full"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
agentId={agent.id.toString()}
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
onScheduleCreated={(schedule) =>
handleSelectRun(schedule.id, "scheduled")
}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
</div>
@@ -109,6 +127,21 @@ export function NewAgentLibraryView() {
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "templates" ? (
<SelectedTemplateView
agent={agent}
templateId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
onSwitchToRunsTab={() => setActiveTab("runs")}
/>
) : activeTab === "triggers" ? (
<SelectedTriggerView
agent={agent}
triggerId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onSwitchToRunsTab={() => setActiveTab("runs")}
/>
) : (
<SelectedRunView
agent={agent}
@@ -127,9 +160,18 @@ export function NewAgentLibraryView() {
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTemplates />
</SelectedViewLayout>
) : activeTab === "triggers" ? (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTriggers />
</SelectedViewLayout>
) : (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTasks agent={agent} />
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</SelectedViewLayout>
)}
</div>

View File

@@ -1,13 +1,11 @@
"use client";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
import {
getAgentCredentialsFields,
getAgentInputFields,
renderValue,
} from "./helpers";
import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
type Props = {
agent: LibraryAgent;
@@ -20,16 +18,28 @@ export function AgentInputsReadOnly({
inputs,
credentialInputs,
}: Props) {
const fields = getAgentInputFields(agent);
const credentialFields = getAgentCredentialsFields(agent);
const inputEntries = Object.entries(fields);
const credentialEntries = Object.entries(credentialFields);
const inputFields = getAgentInputFields(agent);
const credentialFieldEntries = Object.entries(
getAgentCredentialsFields(agent),
);
const hasInputs = inputs && inputEntries.length > 0;
const hasCredentials = credentialInputs && credentialEntries.length > 0;
const inputEntries =
inputs &&
Object.entries(inputs).map(([key, value]) => ({
key,
schema: inputFields[key],
value,
}));
const hasInputs = inputEntries && inputEntries.length > 0;
const hasCredentials = credentialInputs && credentialFieldEntries.length > 0;
if (!hasInputs && !hasCredentials) {
return <div className="text-neutral-600">No input for this run.</div>;
return (
<Text variant="body" className="text-zinc-700">
No input for this run.
</Text>
);
}
return (
@@ -37,14 +47,20 @@ export function AgentInputsReadOnly({
{/* Regular inputs */}
{hasInputs && (
<div className="flex flex-col gap-4">
{inputEntries.map(([key, sub]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{sub?.title || key}</label>
<p className="whitespace-pre-wrap break-words text-sm text-neutral-700">
{renderValue((inputs as Record<string, any>)[key])}
</p>
</div>
))}
{inputEntries.map(({ key, schema, value }) => {
if (!schema) return null;
return (
<RunAgentInputs
key={key}
schema={schema}
value={value}
placeholder={schema.description}
onChange={() => {}}
readOnly={true}
/>
);
})}
</div>
)}
@@ -52,7 +68,7 @@ export function AgentInputsReadOnly({
{hasCredentials && (
<div className="flex flex-col gap-6">
{hasInputs && <div className="border-t border-neutral-200 pt-4" />}
{credentialEntries.map(([key, inputSubSchema]) => {
{credentialFieldEntries.map(([key, inputSubSchema]) => {
const credential = credentialInputs![key];
if (!credential) return null;

View File

@@ -13,7 +13,8 @@ export function getCredentialTypeDisplayName(type: string): string {
}
export function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const schema = agent.input_schema as unknown as {
const schema = (agent.trigger_setup_info?.config_schema ??
agent.input_schema) as unknown as {
properties?: Record<string, any>;
} | null;
if (!schema || !schema.properties) return {};

View File

@@ -62,12 +62,15 @@ export function CredentialRow({
</div>
<IconKey className="h-5 w-5 shrink-0 text-zinc-800" />
<div className="flex min-w-0 flex-1 flex-nowrap items-center gap-4">
<Text variant="body" className="tracking-tight">
<Text
variant="body"
className="line-clamp-1 flex-[0_0_50%] text-ellipsis tracking-tight"
>
{getCredentialDisplayName(credential, displayName)}
</Text>
<Text
variant="large"
className="relative top-1 font-mono tracking-tight"
className="relative top-1 flex-[0_0_40%] overflow-hidden font-mono tracking-tight"
>
{"*".repeat(MASKED_KEY_LENGTH)}
</Text>

View File

@@ -48,8 +48,8 @@ export function CredentialsSelect({
onValueChange={(value) => onSelectCredential(value)}
>
<SelectTrigger className="h-auto min-h-12 w-full rounded-medium border-zinc-200 p-0 pr-4 shadow-none">
<SelectValue asChild>
{selectedCredentials ? (
{selectedCredentials ? (
<SelectValue key={selectedCredentials.id} asChild>
<CredentialRow
credential={{
id: selectedCredentials.id,
@@ -64,10 +64,10 @@ export function CredentialsSelect({
readOnly={readOnly}
asSelectTrigger={true}
/>
) : (
<Text variant="large">Select credential</Text>
)}
</SelectValue>
</SelectValue>
) : (
<SelectValue key="placeholder" placeholder="Select credential" />
)}
</SelectTrigger>
<SelectContent>
{credentials.map((credential) => (

View File

@@ -75,11 +75,11 @@ export function getActionButtonText(
hasExistingCredentials: boolean,
): string {
if (hasExistingCredentials) {
if (supportsOAuth2) return "Connect a different account";
if (supportsApiKey) return "Use a different API key";
if (supportsUserPassword) return "Use a different username and password";
if (supportsHostScoped) return "Use different headers";
return "Add credentials";
if (supportsOAuth2) return "Connect another account";
if (supportsApiKey) return "Use a new API key";
if (supportsUserPassword) return "Add a new username and password";
if (supportsHostScoped) return "Add new headers";
return "Add new credentials";
} else {
if (supportsOAuth2) return "Add account";
if (supportsApiKey) return "Add API key";

View File

@@ -9,6 +9,7 @@ import { Button } from "@/components/atoms/Button/Button";
import { FileInput } from "@/components/atoms/FileInput/FileInput";
import { Switch } from "@/components/atoms/Switch/Switch";
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { TimePicker } from "@/components/molecules/TimePicker/TimePicker";
import {
BlockIOObjectSubSchema,
@@ -32,6 +33,7 @@ interface Props {
value?: any;
placeholder?: string;
onChange: (value: any) => void;
readOnly?: boolean;
}
/**
@@ -44,6 +46,7 @@ export function RunAgentInputs({
value,
placeholder,
onChange,
readOnly = false,
...props
}: Props & React.HTMLAttributes<HTMLElement>) {
const { handleUploadFile, uploadProgress } = useRunAgentInputs();
@@ -62,7 +65,6 @@ export function RunAgentInputs({
id={`${baseId}-number`}
label={schema.title ?? placeholder ?? "Number"}
hideLabel
size="small"
type="number"
value={value ?? ""}
placeholder={placeholder || "Enter number"}
@@ -80,7 +82,6 @@ export function RunAgentInputs({
id={`${baseId}-textarea`}
label={schema.title ?? placeholder ?? "Text"}
hideLabel
size="small"
type="textarea"
rows={3}
value={value ?? ""}
@@ -130,7 +131,6 @@ export function RunAgentInputs({
id={`${baseId}-date`}
label={schema.title ?? placeholder ?? "Date"}
hideLabel
size="small"
type="date"
value={value ? format(value as Date, "yyyy-MM-dd") : ""}
onChange={(e) => {
@@ -159,7 +159,6 @@ export function RunAgentInputs({
id={`${baseId}-datetime`}
label={schema.title ?? placeholder ?? "Date time"}
hideLabel
size="small"
type="datetime-local"
value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -194,7 +193,6 @@ export function RunAgentInputs({
label={schema.title ?? placeholder ?? "Select"}
hideLabel
value={value ?? ""}
size="small"
onValueChange={(val: string) => onChange(val)}
placeholder={placeholder || "Select an option"}
options={schema.enum
@@ -217,7 +215,6 @@ export function RunAgentInputs({
items={allKeys.map((key) => ({
value: key,
label: _schema.properties[key]?.title ?? key,
size: "small",
}))}
selectedValues={selectedValues}
onChange={(values: string[]) =>
@@ -336,7 +333,6 @@ export function RunAgentInputs({
id={`${baseId}-text`}
label={schema.title ?? placeholder ?? "Text"}
hideLabel
size="small"
type="text"
value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -347,6 +343,17 @@ export function RunAgentInputs({
}
return (
<div className="no-drag relative flex w-full">{innerInputElement}</div>
<div className="flex w-full flex-col gap-0 space-y-2">
<label className="large-medium flex items-center gap-1 font-medium">
{schema.title || placeholder}
<InformationTooltip description={schema.description} />
</label>
<div
className="no-drag relative flex w-full"
style={readOnly ? { pointerEvents: "none", opacity: 0.7 } : undefined}
>
{innerInputElement}
</div>
</div>
);
}

View File

@@ -3,7 +3,14 @@
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useState } from "react";
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
@@ -16,16 +23,20 @@ import { useAgentRunModal } from "./useAgentRunModal";
interface Props {
triggerSlot: React.ReactNode;
agent: LibraryAgent;
agentId: string;
agentVersion?: number;
initialInputValues?: Record<string, any>;
initialInputCredentials?: Record<string, any>;
onRunCreated?: (execution: GraphExecutionMeta) => void;
onTriggerSetup?: (preset: LibraryAgentPreset) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
}
export function RunAgentModal({
triggerSlot,
agent,
initialInputValues,
initialInputCredentials,
onRunCreated,
onTriggerSetup,
onScheduleCreated,
}: Props) {
const {
@@ -65,6 +76,9 @@ export function RunAgentModal({
handleRun,
} = useAgentRunModal(agent, {
onRun: onRunCreated,
onSetupTrigger: onTriggerSetup,
initialInputValues,
initialInputCredentials,
});
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
@@ -73,6 +87,8 @@ export function RunAgentModal({
Object.keys(agentInputFields || {}).length > 0 ||
Object.keys(agentCredentialsInputFields || {}).length > 0;
const isTriggerRunType = defaultRunType.includes("trigger");
function handleInputChange(key: string, value: string) {
setInputValues((prev) => ({
...prev,
@@ -147,15 +163,45 @@ export function RunAgentModal({
<Dialog.Footer className="mt-6 bg-white pt-4">
<div className="flex items-center justify-end gap-3">
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting || isSettingUpTrigger || !allRequiredInputsAreSet
}
>
Schedule Task
</Button>
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
Schedule Task
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
<p>
Please set up all required inputs and credentials before
scheduling
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
Schedule Task
</Button>
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}

View File

@@ -26,7 +26,8 @@ export function ModalRunSection() {
return (
<div className="flex flex-col gap-4">
{defaultRunType === "automatic-trigger" ? (
{defaultRunType === "automatic-trigger" ||
defaultRunType === "manual-trigger" ? (
<ModalSection
title="Task Trigger"
subtitle="Set up a trigger for the agent to run this task automatically"
@@ -69,25 +70,18 @@ export function ModalRunSection() {
{inputFields.length > 0 ? (
<ModalSection
title="Task Setup"
subtitle="Enter the information needed for the agent to run this task"
title="Task Inputs"
subtitle="Enter the information you want to provide to the agent for this task"
>
{/* Regular inputs */}
{inputFields.map(([key, inputSubSchema]) => (
<div key={key} className="flex w-full flex-col gap-0 space-y-2">
<label className="flex items-center gap-1 text-sm font-medium">
{inputSubSchema.title || key}
<InformationTooltip description={inputSubSchema.description} />
</label>
<RunAgentInputs
schema={inputSubSchema}
value={inputValues[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
data-testid={`agent-input-${key}`}
/>
</div>
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={inputValues[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
data-testid={`agent-input-${key}`}
/>
))}
</ModalSection>
) : null}

View File

@@ -24,7 +24,8 @@ export function RunActions({
disabled={!isRunReady || isExecuting || isSettingUpTrigger}
loading={isExecuting || isSettingUpTrigger}
>
{defaultRunType === "automatic-trigger"
{defaultRunType === "automatic-trigger" ||
defaultRunType === "manual-trigger"
? "Set up Trigger"
: "Start Task"}
</Button>

View File

@@ -1,6 +1,6 @@
export function WebhookTriggerBanner() {
return (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<div className="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg

View File

@@ -1,14 +1,11 @@
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
getGetV1ListGraphExecutionsQueryKey,
usePostV1ExecuteGraphAgent,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets/presets";
import {
getGetV1ListExecutionSchedulesForAGraphQueryKey,
usePostV1CreateExecutionSchedule as useCreateSchedule,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
getGetV2ListPresetsQueryKey,
usePostV2SetupTrigger,
} from "@/app/api/__generated__/endpoints/presets/presets";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
@@ -16,7 +13,7 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
import { isEmpty } from "@/lib/utils";
import { analytics } from "@/services/analytics";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { showExecutionErrorToast } from "./errorHelpers";
export type RunVariant =
@@ -27,8 +24,9 @@ export type RunVariant =
interface UseAgentRunModalCallbacks {
onRun?: (execution: GraphExecutionMeta) => void;
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
onSetupTrigger?: (preset: LibraryAgentPreset) => void;
initialInputValues?: Record<string, any>;
initialInputCredentials?: Record<string, any>;
}
export function useAgentRunModal(
@@ -38,31 +36,28 @@ export function useAgentRunModal(
const { toast } = useToast();
const queryClient = useQueryClient();
const [isOpen, setIsOpen] = useState(false);
const [showScheduleView, setShowScheduleView] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const [inputValues, setInputValues] = useState<Record<string, any>>(
callbacks?.initialInputValues || {},
);
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
{},
callbacks?.initialInputCredentials || {},
);
const [presetName, setPresetName] = useState<string>("");
const [presetDescription, setPresetDescription] = useState<string>("");
const defaultScheduleName = useMemo(() => `Run ${agent.name}`, [agent.name]);
const [scheduleName, setScheduleName] = useState(defaultScheduleName);
const [cronExpression, setCronExpression] = useState(
agent.recommended_schedule_cron || "0 9 * * 1",
);
// Get user timezone for scheduling
const { data: userTimezone } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
// Determine the default run type based on agent capabilities
const defaultRunType: RunVariant = agent.has_external_trigger
? "automatic-trigger"
const defaultRunType: RunVariant = agent.trigger_setup_info
? agent.trigger_setup_info.credentials_input_name
? "automatic-trigger"
: "manual-trigger"
: "manual";
// Update input values/credentials if template is selected/unselected
useEffect(() => {
setInputValues(callbacks?.initialInputValues || {});
setInputCredentials(callbacks?.initialInputCredentials || {});
}, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]);
// API mutations
const executeGraphMutation = usePostV1ExecuteGraphAgent({
mutation: {
@@ -71,13 +66,11 @@ export function useAgentRunModal(
toast({
title: "Agent execution started",
});
callbacks?.onRun?.(response.data as unknown as GraphExecutionMeta);
// Invalidate runs list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
agent.graph_id,
).queryKey,
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
});
callbacks?.onRun?.(response.data);
analytics.sendDatafastEvent("run_agent", {
name: agent.name,
id: agent.graph_id,
@@ -94,45 +87,16 @@ export function useAgentRunModal(
},
});
const createScheduleMutation = useCreateSchedule({
const setupTriggerMutation = usePostV2SetupTrigger({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Schedule created",
});
callbacks?.onCreateSchedule?.(response.data);
// Invalidate schedules list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
agent.graph_id,
),
});
analytics.sendDatafastEvent("schedule_agent", {
name: agent.name,
id: agent.graph_id,
cronExpression: cronExpression,
});
setIsOpen(false);
}
},
onError: (error: any) => {
toast({
title: "❌ Failed to create schedule",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const setupTriggerMutation = usePostV2SetupTrigger({
mutation: {
onSuccess: (response: any) => {
if (response.status === 200) {
toast({
title: "Trigger setup complete",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
});
callbacks?.onSetupTrigger?.(response.data);
setIsOpen(false);
}
@@ -147,11 +111,13 @@ export function useAgentRunModal(
},
});
// Input schema validation
const agentInputSchema = useMemo(
() => agent.input_schema || { properties: {}, required: [] },
[agent.input_schema],
);
// Input schema validation (use trigger schema for triggered agents)
const agentInputSchema = useMemo(() => {
if (agent.trigger_setup_info?.config_schema) {
return agent.trigger_setup_info.config_schema;
}
return agent.input_schema || { properties: {}, required: [] };
}, [agent.input_schema, agent.trigger_setup_info]);
const agentInputFields = useMemo(() => {
if (
@@ -220,33 +186,25 @@ export function useAgentRunModal(
[allRequiredInputsAreSetRaw, credentialsRequired, allCredentialsAreSet],
);
const notifyMissingRequirements = useCallback(
(needScheduleName: boolean = false) => {
const allMissingFields = (
needScheduleName && !scheduleName ? ["schedule_name"] : []
)
.concat(missingInputs)
.concat(
credentialsRequired && !allCredentialsAreSet
? missingCredentials.map((k) => `credentials:${k}`)
: [],
);
const notifyMissingRequirements = useCallback(() => {
const allMissingFields = missingInputs.concat(
credentialsRequired && !allCredentialsAreSet
? missingCredentials.map((k) => `credentials:${k}`)
: [],
);
toast({
title: "⚠️ Missing required inputs",
description: `Please provide: ${allMissingFields.map((k) => `"${k}"`).join(", ")}`,
variant: "destructive",
});
},
[
missingInputs,
scheduleName,
toast,
credentialsRequired,
allCredentialsAreSet,
missingCredentials,
],
);
toast({
title: "⚠️ Missing required inputs",
description: `Please provide: ${allMissingFields.map((k) => `"${k}"`).join(", ")}`,
variant: "destructive",
});
}, [
missingInputs,
toast,
credentialsRequired,
allCredentialsAreSet,
missingCredentials,
]);
// Action handlers
const handleRun = useCallback(() => {
@@ -255,9 +213,12 @@ export function useAgentRunModal(
return;
}
if (defaultRunType === "automatic-trigger") {
if (
defaultRunType === "automatic-trigger" ||
defaultRunType === "manual-trigger"
) {
// Setup trigger
if (!scheduleName.trim()) {
if (!presetName.trim()) {
toast({
title: "⚠️ Trigger name required",
description: "Please provide a name for your trigger.",
@@ -268,7 +229,7 @@ export function useAgentRunModal(
setupTriggerMutation.mutate({
data: {
name: presetName || scheduleName,
name: presetName,
description: presetDescription || `Trigger for ${agent.name}`,
graph_id: agent.graph_id,
graph_version: agent.graph_version,
@@ -291,7 +252,6 @@ export function useAgentRunModal(
}, [
allRequiredInputsAreSet,
defaultRunType,
scheduleName,
inputValues,
inputCredentials,
agent,
@@ -303,70 +263,6 @@ export function useAgentRunModal(
toast,
]);
const handleSchedule = useCallback(() => {
if (!allRequiredInputsAreSet) {
notifyMissingRequirements(true);
return;
}
if (!scheduleName.trim()) {
toast({
title: "⚠️ Schedule name required",
description: "Please provide a name for your schedule.",
variant: "destructive",
});
return;
}
createScheduleMutation.mutate({
graphId: agent.graph_id,
data: {
name: presetName || scheduleName,
cron: cronExpression,
inputs: inputValues,
graph_version: agent.graph_version,
credentials: inputCredentials,
timezone:
userTimezone && userTimezone !== "not-set" ? userTimezone : undefined,
},
});
}, [
allRequiredInputsAreSet,
scheduleName,
cronExpression,
inputValues,
inputCredentials,
agent,
notifyMissingRequirements,
createScheduleMutation,
toast,
userTimezone,
]);
function handleShowSchedule() {
// Initialize with sensible defaults when entering schedule view
setScheduleName((prev) => prev || defaultScheduleName);
setCronExpression(
(prev) => prev || agent.recommended_schedule_cron || "0 9 * * 1",
);
setShowScheduleView(true);
}
function handleGoBack() {
setShowScheduleView(false);
// Reset schedule fields on exit
setScheduleName(defaultScheduleName);
setCronExpression(agent.recommended_schedule_cron || "0 9 * * 1");
}
function handleSetScheduleName(name: string) {
setScheduleName(name);
}
function handleSetCronExpression(expression: string) {
setCronExpression(expression);
}
const hasInputFields = useMemo(() => {
return Object.keys(agentInputFields).length > 0;
}, [agentInputFields]);
@@ -375,10 +271,9 @@ export function useAgentRunModal(
// UI state
isOpen,
setIsOpen,
showScheduleView,
// Run mode
defaultRunType,
defaultRunType: defaultRunType as RunVariant,
// Form: regular inputs
inputValues,
@@ -394,10 +289,6 @@ export function useAgentRunModal(
setPresetName,
setPresetDescription,
// Scheduling
scheduleName,
cronExpression,
// Validation/readiness
allRequiredInputsAreSet,
missingInputs,
@@ -409,15 +300,9 @@ export function useAgentRunModal(
// Async states
isExecuting: executeGraphMutation.isPending,
isCreatingSchedule: createScheduleMutation.isPending,
isSettingUpTrigger: setupTriggerMutation.isPending,
// Actions
handleRun,
handleSchedule,
handleShowSchedule,
handleGoBack,
handleSetScheduleName,
handleSetCronExpression,
};
}

View File

@@ -1,17 +1,58 @@
"use client";
import { getV1GetGraphVersion } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { ShowMoreText } from "@/components/molecules/ShowMoreText/ShowMoreText";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { exportAsJSONFile } from "@/lib/utils";
import { formatDate } from "@/lib/utils/time";
import Link from "next/link";
import { RunAgentModal } from "../modals/RunAgentModal/RunAgentModal";
import { RunDetailCard } from "../selected-views/RunDetailCard/RunDetailCard";
import { EmptyTasksIllustration } from "./EmptyTasksIllustration";
type Props = {
agent: LibraryAgent;
onRun?: (run: GraphExecutionMeta) => void;
onTriggerSetup?: (preset: LibraryAgentPreset) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
};
export function EmptyTasks({ agent }: Props) {
export function EmptyTasks({
agent,
onRun,
onTriggerSetup,
onScheduleCreated,
}: Props) {
const { toast } = useToast();
async function handleExport() {
try {
const res = await getV1GetGraphVersion(
agent.graph_id,
agent.graph_version,
{ for_export: true },
);
if (res.status === 200) {
const filename = `${agent.name}_v${agent.graph_version}.json`;
exportAsJSONFile(res.data as any, filename);
toast({ title: "Agent exported" });
} else {
toast({ title: "Failed to export agent", variant: "destructive" });
}
} catch (e: any) {
toast({
title: "Failed to export agent",
description: e?.message,
variant: "destructive",
});
}
}
const isPublished = Boolean(agent.marketplace_listing);
const createdAt = formatDate(agent.created_at);
const updatedAt = formatDate(agent.updated_at);
@@ -45,7 +86,9 @@ export function EmptyTasks({ agent }: Props) {
</Button>
}
agent={agent}
agentId={agent.id.toString()}
onRunCreated={onRun}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</div>
</div>
@@ -93,10 +136,15 @@ export function EmptyTasks({ agent }: Props) {
) : null}
</div>
<div className="mt-4 flex items-center gap-2">
<Button variant="secondary" size="small">
Edit agent
<Button variant="secondary" size="small" asChild>
<Link
href={`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`}
target="_blank"
>
Edit agent
</Link>
</Button>
<Button variant="secondary" size="small">
<Button variant="secondary" size="small" onClick={handleExport}>
Export agent to file
</Button>
</div>

View File

@@ -0,0 +1,323 @@
import { Text } from "@/components/atoms/Text/Text";
export function EmptyTriggers() {
return (
<div className="flex h-full flex-col items-center justify-center gap-20">
<div>
<svg
width="342"
height="211"
viewBox="0 0 342 211"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M212.148 114.272C193.65 114.272 175.622 86.479 175.622 86.479L169.237 78.4039L164.636 72.6762L163.979 71.8311L136.843 37.5588L135.247 35.4931C134.683 34.8358 134.026 34.0847 133.369 33.2396C128.58 26.9485 122.383 15.8687 126.702 10.0471C131.585 3.38044 145.857 -1.3144 155.998 2.15978C166.139 5.63396 170.646 10.5166 170.646 10.5166C170.646 10.5166 184.918 12.3945 193.65 40.1879C202.383 67.9814 227.078 55.7748 236.467 80.9391C242.571 97.3711 230.552 114.366 212.054 114.366L212.148 114.272Z"
fill="#FF9C0F"
/>
<path
d="M212.148 114.554C193.744 114.554 175.622 86.8547 175.434 86.573L135.059 35.5871C134.496 34.9298 133.932 34.1787 133.181 33.2397C121.913 18.3101 125.294 11.4557 126.514 9.67163C131.397 3.09885 145.669 -1.97157 156.092 1.6904C165.481 4.88289 170.176 9.38994 170.74 10.0472C171.397 10.1411 174.683 10.8923 179.003 14.5543C183.322 18.2162 189.331 25.5402 193.838 40.0003C198.251 54.2726 207.172 57.9345 215.716 61.5026C223.791 64.8829 232.148 68.3571 236.749 80.6575C239.66 88.451 238.627 96.7139 234.026 103.381C229.143 110.423 221.162 114.46 212.148 114.46C212.148 114.46 212.148 114.46 212.054 114.46L212.148 114.554ZM148.392 1.12703C139.754 1.12703 130.646 5.16458 126.984 10.1411C123.134 15.3054 127.923 25.3524 133.65 32.958C134.308 33.897 134.965 34.6481 135.434 35.2115L137.031 37.2773L175.81 86.2913C175.998 86.573 193.932 113.897 211.96 113.991C211.96 113.991 211.96 113.991 212.054 113.991C220.88 113.991 228.768 110.047 233.557 103.193C238.064 96.7139 239.096 88.5449 236.28 81.0331C231.772 69.0144 223.885 65.728 215.622 62.1599C206.984 58.4979 197.97 54.7421 193.463 40.282C188.956 26.0097 183.04 18.7796 178.815 15.2115C174.308 11.3618 170.74 10.7984 170.646 10.7984C170.646 10.7984 170.552 10.7984 170.458 10.7984C170.458 10.7984 165.857 5.91576 155.904 2.53548C153.557 1.69041 150.927 1.31482 148.298 1.31482L148.392 1.12703Z"
fill="#101720"
/>
<path
d="M212.148 114.272C193.651 114.272 175.622 86.479 175.622 86.479H170.176L169.143 78.4039L168.862 76.4321L174.59 68.3569C184.261 79.5306 205.294 90.986 219.848 88.9203C234.402 86.7607 236.28 80.9391 236.28 80.9391C242.383 97.371 230.364 114.366 211.867 114.366L212.148 114.272Z"
fill="#101720"
/>
<path
d="M212.148 114.554C194.402 114.554 176.843 88.8264 175.528 86.7606H170.27C170.082 86.7606 169.988 86.6668 169.988 86.479L168.768 76.432C168.768 76.432 168.768 76.3381 168.768 76.2442L174.496 68.1691C174.496 68.1691 174.589 68.0752 174.683 68.0752C174.683 68.0752 174.871 68.0752 174.871 68.1691C184.73 79.5306 205.669 90.7043 219.942 88.5447C234.12 86.479 236.186 80.8451 236.186 80.7513C236.186 80.6574 236.373 80.5635 236.467 80.5635C236.467 80.5635 236.655 80.5635 236.655 80.6573C239.566 88.4508 238.533 96.7137 233.932 103.38C229.05 110.423 221.068 114.46 212.054 114.46L212.148 114.554ZM170.458 86.1973H175.622C175.716 86.1973 175.81 86.1973 175.904 86.2912C176.092 86.5728 194.026 113.897 212.054 113.991C220.881 113.991 228.768 110.047 233.557 103.193C237.97 96.8076 239.003 89.0142 236.467 81.5963C235.528 83.1926 231.96 87.4179 220.035 89.1081C205.669 91.2677 184.73 80.1879 174.683 68.7325L169.143 76.432L170.364 86.1973H170.458Z"
fill="#101720"
/>
<path
d="M131.021 51.3617C131.021 51.3617 129.519 63.5683 134.777 69.0143C140.411 74.8359 154.402 73.5213 154.402 73.5213L152.524 86.4791H175.528L174.683 68.2631L167.078 57.3711C174.308 52.864 171.866 44.6011 167.453 42.1598C166.327 41.5964 165.106 41.3147 163.885 41.5964C157.782 42.911 158.345 53.1457 158.345 53.1457H154.402L150.552 39.0612C145.012 36.6199 138.251 33.5213 135.247 24.8828C135.247 25.1645 133.932 28.4509 133.463 32.6762C132.054 44.3194 127.266 45.0706 126.796 47.8875C126.327 50.6105 131.115 51.3617 131.115 51.3617H131.021Z"
fill="#F48282"
/>
<mask
id="mask0_912_14740"
style={{ maskType: "luminance" }}
maskUnits="userSpaceOnUse"
x="126"
y="24"
width="50"
height="63"
>
<path
d="M131.021 51.3615C131.021 51.3615 129.519 63.5681 134.777 69.0141C140.411 74.8357 154.402 73.5212 154.402 73.5212L152.524 86.4789H175.528L174.683 68.3568L167.078 57.4648C174.308 52.9578 171.866 44.6949 167.453 42.2536C166.327 41.5963 165.106 41.4085 163.885 41.6902C157.782 43.0047 158.345 53.2395 158.345 53.2395H154.402L150.552 39.155C145.012 36.7137 138.251 33.6151 135.247 24.9766C135.247 25.2583 133.932 28.5446 133.463 32.77C132.054 44.4132 127.266 45.1644 126.796 47.9813C126.327 50.7043 131.115 51.4554 131.115 51.4554L131.021 51.3615Z"
fill="white"
/>
</mask>
<g mask="url(#mask0_912_14740)">
<path
d="M154.402 53.2393C154.402 53.2393 160.975 56.3378 161.162 61.9716C161.35 70.3284 149.425 72.3942 142.007 72.6759C132.148 73.0515 145.763 87.6993 145.763 87.6993C145.763 87.6993 170.928 84.5068 171.491 84.5068C172.054 84.5068 183.979 83.0984 183.979 83.0984C183.979 83.0984 181.35 47.6993 181.162 47.4176C180.975 47.136 170.176 36.0562 170.176 36.0562L157.313 42.1594L154.402 53.1454"
fill="#2973E8"
/>
</g>
<path
d="M175.622 86.7604H152.618C152.618 86.7604 152.43 86.7604 152.43 86.6665V86.4787L154.214 73.8966C151.96 74.0844 139.942 74.7416 134.683 69.2956C130.646 65.0703 130.552 56.8074 130.646 53.4271C130.646 52.582 130.646 52.0186 130.74 51.7369C129.988 51.5491 127.829 51.0796 126.89 49.7651C126.514 49.2017 126.327 48.6383 126.42 47.9811C126.608 47.0421 127.172 46.2909 128.017 45.3519C129.613 43.474 132.054 40.6571 133.087 33.1454C133.087 33.0515 133.087 32.8637 133.087 32.7698C133.369 30.2346 134.026 27.5116 134.871 25.0703C134.871 24.8825 135.059 24.7886 135.153 24.7886C135.247 24.7886 135.341 24.7886 135.434 24.9764C138.439 33.521 145.2 36.5257 150.552 38.967C150.552 38.967 150.74 39.0609 150.74 39.1548L154.496 52.9576H157.97C157.97 51.1736 157.97 42.6289 163.791 41.4083C165.012 41.1266 166.327 41.4083 167.641 42.0656C170.176 43.474 171.866 46.5726 171.96 49.6712C171.96 52.8637 170.458 55.6806 167.547 57.5585L174.965 68.1689C174.965 68.1689 174.965 68.2627 174.965 68.3566L175.904 86.5726V86.7604C175.904 86.7604 175.81 86.7604 175.716 86.7604H175.622ZM152.899 86.197H175.341L174.496 68.3566L166.984 57.5585V57.3707C166.984 57.3707 166.984 57.1829 167.078 57.1829C169.988 55.305 171.585 52.6759 171.491 49.5773C171.491 46.6665 169.801 43.7557 167.453 42.4412C166.327 41.8778 165.2 41.5961 164.073 41.8778C158.251 43.0984 158.721 53.0515 158.721 53.1454V53.3332C158.721 53.3332 158.627 53.3332 158.533 53.3332H154.589C154.496 53.3332 154.308 53.3332 154.308 53.1454L150.458 39.2487C145.106 36.8073 138.533 33.8966 135.341 25.7275C134.589 27.9811 134.12 30.4224 133.838 32.6759C133.838 32.7698 133.838 32.9576 133.838 33.0515C132.805 40.6571 130.364 43.5679 128.674 45.4459C127.923 46.2909 127.359 46.9482 127.172 47.7933C127.172 48.2628 127.172 48.7322 127.453 49.1078C128.486 50.4224 131.209 50.8919 131.209 50.8919C131.397 50.8919 131.491 51.0796 131.491 51.1735C131.491 51.1735 131.491 51.9247 131.397 53.1454C131.209 56.4318 131.397 64.6008 135.247 68.6383C140.693 74.2721 154.496 73.0515 154.683 73.0515H154.871V73.2393L152.993 85.9153L152.899 86.197Z"
fill="#101720"
/>
<path
d="M134.402 42.7228C134.402 42.7228 136.092 39.6242 140.129 40.1876C144.167 40.751 144.73 43.3801 144.73 43.3801C144.73 43.3801 142.946 46.5726 139.284 46.1031C135.622 45.6336 134.402 42.7228 134.402 42.7228Z"
fill="#FFFFFE"
/>
<mask
id="mask1_912_14740"
style={{ maskType: "luminance" }}
maskUnits="userSpaceOnUse"
x="134"
y="40"
width="11"
height="7"
>
<path
d="M134.402 42.7228C134.402 42.7228 136.092 39.6242 140.129 40.1876C144.167 40.751 144.73 43.3801 144.73 43.3801C144.73 43.3801 142.946 46.5726 139.284 46.1031C135.622 45.6336 134.402 42.7228 134.402 42.7228Z"
fill="white"
/>
</mask>
<g mask="url(#mask1_912_14740)">
<path
d="M138.064 46.2909C139.516 46.2909 140.693 45.1138 140.693 43.6618C140.693 42.2098 139.516 41.0327 138.064 41.0327C136.612 41.0327 135.435 42.2098 135.435 43.6618C135.435 45.1138 136.612 46.2909 138.064 46.2909Z"
fill="#101720"
/>
</g>
<path
d="M139.19 60.1877C139.002 60.1877 138.908 60.0938 138.908 59.906C138.908 58.4037 139.378 57.2769 140.129 56.7136C140.88 56.1502 142.007 56.1502 143.322 56.7136C143.509 56.7136 143.509 56.9013 143.509 57.0891C143.509 57.2769 143.322 57.2769 143.134 57.2769C142.007 56.8074 141.068 56.8074 140.411 57.2769C139.754 57.7464 139.378 58.6854 139.378 59.9999C139.378 61.3145 139.284 60.2816 139.096 60.2816L139.19 60.1877Z"
fill="#101720"
/>
<path
d="M144.543 39.437C144.543 39.437 144.355 39.437 144.355 39.3431C143.604 38.3102 142.101 37.559 140.317 37.3712C138.345 37.1834 136.561 37.7468 135.435 38.7797C135.341 38.8736 135.153 38.8736 135.059 38.7797C134.965 38.6858 134.965 38.498 135.059 38.4041C136.28 37.1834 138.251 36.6201 140.411 36.8079C142.383 36.9957 143.979 37.8407 144.824 39.0614C144.824 39.1553 144.824 39.3431 144.824 39.437C144.824 39.437 144.73 39.437 144.636 39.437H144.543Z"
fill="#101720"
/>
<path
d="M140.223 40.0942C140.223 40.0942 142.101 40.1881 143.51 41.4088C144.918 42.6294 145.294 43.5684 145.763 43.7562L145.2 44.4135C145.2 44.4135 142.946 40.8454 140.223 40.1881V40.0942Z"
fill="#101720"
/>
<path
d="M135.246 25.2581C135.153 25.2581 135.059 25.2581 134.965 25.0703C134.683 24.2252 134.401 23.3801 134.214 22.3473C134.214 22.1595 134.214 22.0656 134.401 21.9717C134.589 21.9717 134.683 21.9717 134.777 22.1595C134.965 23.0984 135.246 23.9435 135.528 24.7886C135.528 24.9764 135.528 25.0703 135.34 25.1642C135.34 25.1642 135.34 25.1642 135.246 25.1642V25.2581Z"
fill="#101720"
/>
<path
d="M154.495 73.5211C162.101 72.9577 168.955 70.3286 168.955 70.3286L153.744 78.5915L154.495 73.5211Z"
fill="#101720"
/>
<path
d="M153.744 78.7794C153.744 78.7794 153.65 78.7794 153.556 78.7794C153.556 78.7794 153.462 78.5916 153.462 78.4977L154.214 73.4273C154.214 73.3334 154.307 73.2395 154.495 73.1456C162.007 72.5822 168.861 69.9531 168.955 69.9531C169.049 69.9531 169.237 69.9531 169.331 70.1409C169.331 70.2348 169.331 70.4226 169.237 70.5165L154.026 78.7794C154.026 78.7794 154.026 78.7794 153.932 78.7794H153.744ZM154.777 73.8029L154.12 78.0282L166.138 71.5494C163.415 72.3945 159.19 73.4273 154.777 73.8029Z"
fill="#101720"
/>
<path
d="M161.35 49.5773C161.162 49.5773 161.068 49.3895 161.162 49.2018C161.82 46.6665 163.416 45.8215 164.355 45.6337C165.669 45.2581 167.078 45.6337 167.923 46.5726C168.017 46.6665 168.017 46.8543 167.923 46.9482C167.829 47.0421 167.641 47.0421 167.547 46.9482C166.89 46.197 165.669 45.9154 164.543 46.1971C163.791 46.3849 162.383 47.0421 161.82 49.3895C161.82 49.4834 161.632 49.5773 161.538 49.5773H161.35Z"
fill="#101720"
/>
<path
d="M164.448 52.5823C164.448 52.5823 164.26 52.5823 164.26 52.4884C164.26 52.3945 164.26 52.2067 164.26 52.1128C166.514 50.4227 165.199 46.0095 165.199 45.9156C165.199 45.7278 165.199 45.6339 165.387 45.54C165.575 45.54 165.669 45.5401 165.763 45.7278C165.763 45.9156 167.265 50.6105 164.636 52.4884C164.636 52.4884 164.542 52.4884 164.448 52.4884V52.5823Z"
fill="#101720"
/>
<path
d="M131.022 51.3618L133.463 51.7374L130.928 53.3336L131.022 51.3618Z"
fill="#101720"
/>
<path
d="M130.928 53.6151C130.928 53.6151 130.834 53.6151 130.74 53.6151C130.74 53.6151 130.552 53.4274 130.646 53.3335L130.834 51.3616C130.834 51.3616 130.834 51.1738 130.928 51.1738C130.928 51.1738 131.022 51.1738 131.115 51.1738L133.463 51.4555C133.557 51.4555 133.651 51.5494 133.745 51.6433C133.838 51.7372 133.745 51.8311 133.651 51.925L131.115 53.6151C131.115 53.6151 131.022 53.6151 130.928 53.6151ZM131.303 51.7372V52.864L132.618 51.925L131.303 51.7372Z"
fill="#101720"
/>
<path
d="M167.547 14.2725C167.547 14.2725 168.205 26.479 173.463 31.5494C178.721 36.6199 193.557 40.1879 193.557 40.1879L190.928 32.9579L167.453 14.2725H167.547Z"
fill="#101720"
/>
<path
d="M193.65 40.3757C193.087 40.1879 178.721 36.8076 173.369 31.7372C168.111 26.5729 167.359 14.7419 167.359 14.2724C167.359 14.1785 167.359 14.0846 167.547 13.9907C167.641 13.9907 167.735 13.9907 167.829 13.9907L191.303 32.6762C191.303 32.6762 191.303 32.6762 191.303 32.7701L193.932 40.0001C193.932 40.094 193.932 40.1879 193.932 40.2818C193.932 40.2818 193.838 40.3757 193.744 40.3757H193.65ZM167.923 14.8358C168.111 17.371 169.331 27.0424 173.744 31.3616C178.345 35.7748 190.552 39.0611 193.181 39.7184L190.834 33.0518L167.923 14.7419V14.8358Z"
fill="#101720"
/>
<path
d="M165.2 6.29084C165.2 6.29084 163.979 16.2439 170.834 24.9763C177.688 33.7087 191.115 32.9575 191.115 32.9575C191.115 32.9575 204.824 15.7744 193.745 6.29084C177.594 -7.51198 165.2 6.29084 165.2 6.29084Z"
fill="#FF9C0F"
/>
<path
d="M190.27 33.2395C189.049 33.2395 186.42 33.2395 183.416 32.4883C179.566 31.6432 174.12 29.6714 170.552 25.1644C164.073 16.9015 164.73 7.32399 164.824 6.38503V6.19724C164.918 6.10334 177.594 -7.69948 193.744 6.19724C204.824 15.6808 191.209 33.0517 191.115 33.2395C191.115 33.2395 191.021 33.3334 190.927 33.3334C190.927 33.3334 190.646 33.3334 190.176 33.3334L190.27 33.2395ZM165.481 6.38503C165.388 7.41789 164.73 16.7137 171.021 24.7888C177.312 32.8639 189.519 32.6761 190.927 32.6761C192.054 31.2677 203.791 15.3052 193.557 6.57282C178.251 -6.47883 166.608 5.25827 165.481 6.47893V6.38503Z"
fill="#101720"
/>
<path
d="M194.12 27.5121C189.519 27.5121 176.937 23.7563 173.463 15.1178C173.463 14.93 173.463 14.8361 173.65 14.7422C173.838 14.7422 173.932 14.7422 174.026 14.93C177.594 23.5685 190.927 27.2304 194.683 26.9488C194.871 26.9488 194.965 27.0426 194.965 27.2304C194.965 27.4182 194.871 27.5121 194.683 27.5121C194.496 27.5121 194.308 27.5121 194.12 27.5121Z"
fill="#101720"
/>
<path
d="M327.547 78.0281C327.547 78.0281 316.843 189.577 289.895 203.005C262.946 216.526 234.777 185.915 234.777 185.915V209.953H103.228V187.136C103.228 187.136 51.7726 204.319 39.0965 176.15C26.4204 147.981 13.2749 78.0281 13.2749 78.0281L65.2937 77.8403L84.9181 138.122C84.9181 138.122 107.547 110.422 134.026 90.2347C160.411 70.0469 197.782 78.2159 214.026 93.3333C230.364 108.451 259.848 148.92 259.848 148.92L275.247 78.122H327.547V78.0281Z"
fill="#FFFFFE"
/>
<path
d="M234.777 210.235H103.228C103.04 210.235 102.946 210.141 102.946 209.953V187.512C100.693 188.169 88.862 191.831 75.9042 192.488C56.8432 193.521 44.0732 187.887 38.815 176.244C26.3267 148.357 13.0873 78.7793 12.9934 78.0281V77.8403C12.9934 77.8403 13.0873 77.7464 13.1812 77.7464L65.2 77.5586C65.2939 77.5586 65.3878 77.5586 65.4817 77.7464L85.0122 137.559C87.923 134.084 109.425 108.732 133.838 90.0468C161.069 69.2018 198.627 78.6854 214.214 93.1455C229.425 107.23 256.28 143.568 259.754 148.263L274.965 78.0281C274.965 77.9342 275.153 77.8403 275.247 77.8403H327.547C327.735 77.8403 327.829 77.9342 327.829 78.122C327.829 78.3098 327.829 78.122 327.829 78.2159C327.641 79.8121 324.918 107.136 319.097 135.587C311.022 174.929 301.256 197.746 289.989 203.38C265.106 215.868 238.909 190.704 234.965 186.76V210.047C234.965 210.235 234.871 210.329 234.684 210.329L234.777 210.235ZM103.698 210.235H234.59V185.915C234.59 185.821 234.59 185.728 234.777 185.634C234.871 185.634 234.965 185.634 235.059 185.634C235.341 185.915 263.416 215.868 289.895 202.629C315.716 189.765 326.702 84.6947 327.36 78.122H275.623L260.317 148.732C260.317 148.826 260.223 148.92 260.13 148.92C260.036 148.92 259.942 148.92 259.848 148.826C259.566 148.451 230.177 108.263 214.026 93.3332C198.533 78.967 161.256 69.6713 134.308 90.2346C108.205 110.235 85.4817 137.84 85.2939 138.122C85.2939 138.122 85.1061 138.216 85.0122 138.216C84.9183 138.216 84.8244 138.216 84.8244 138.028L65.2 77.9342L13.7446 78.122C14.7774 83.3802 27.4535 149.014 39.4723 175.868C51.8666 203.474 102.759 186.854 103.322 186.667C103.322 186.667 103.51 186.667 103.604 186.667C103.604 186.667 103.698 186.76 103.698 186.854V209.39V210.235Z"
fill="#101720"
/>
<path
d="M104.261 156.338C103.697 164.977 103.322 175.587 103.322 187.137L98.7209 188.263L104.355 156.338H104.261Z"
fill="#101720"
/>
<path
d="M98.7207 188.545C98.7207 188.545 98.6268 188.545 98.5329 188.545C98.5329 188.545 98.439 188.357 98.5329 188.263L104.073 156.432C104.073 156.338 104.073 156.244 104.261 156.15C104.355 156.15 104.448 156.15 104.542 156.15C104.542 156.15 104.636 156.338 104.542 156.432C103.885 166.949 103.603 177.277 103.603 187.136C103.603 196.995 103.603 187.418 103.416 187.418L98.8146 188.545H98.7207ZM103.697 161.221L99.0024 187.887L102.946 186.855C102.946 178.498 103.228 169.953 103.603 161.221H103.697Z"
fill="#101720"
/>
<path
d="M104.261 156.62C104.073 156.62 103.979 156.432 103.979 156.338C104.824 143.286 105.857 134.648 105.857 134.648C105.857 134.46 106.045 134.366 106.139 134.366C106.327 134.366 106.421 134.554 106.421 134.648C106.421 134.648 105.388 143.38 104.543 156.338C104.543 156.526 104.449 156.62 104.261 156.62Z"
fill="#101720"
/>
<path
d="M240.411 191.174L234.778 185.916C234.778 174.93 234.214 163.38 233.557 153.709L240.411 191.08V191.174Z"
fill="#101720"
/>
<path
d="M240.411 191.456C240.411 191.456 240.317 191.456 240.223 191.456L234.589 186.197C234.589 186.197 234.589 186.103 234.589 186.01C234.589 176.714 234.214 165.916 233.369 153.897C233.369 153.709 233.463 153.615 233.65 153.615C233.838 153.615 233.932 153.615 233.932 153.803L240.786 191.174C240.786 191.268 240.786 191.456 240.599 191.456C240.599 191.456 240.599 191.456 240.505 191.456H240.411ZM235.059 185.822L239.941 190.423L234.12 158.592C234.777 168.639 235.059 177.841 235.059 185.822Z"
fill="#101720"
/>
<path
d="M233.557 154.085C233.369 154.085 233.275 153.991 233.275 153.803C232.336 140.658 231.303 130.705 231.303 130.611C231.303 130.423 231.303 130.329 231.585 130.329C231.773 130.329 231.867 130.423 231.867 130.611C231.867 130.705 232.993 140.564 233.932 153.803C233.932 153.991 233.838 154.085 233.651 154.085H233.557Z"
fill="#101720"
/>
<path
d="M233.556 154.084C233.369 154.084 233.275 153.99 233.275 153.803C233.275 153.615 233.369 153.521 233.556 153.521C233.744 153.521 233.838 153.615 233.838 153.803C233.838 153.99 233.744 154.084 233.556 154.084Z"
fill="#101720"
/>
<path
d="M260.505 158.967C256.092 158.967 249.989 149.672 244.73 141.597C243.322 139.531 242.007 137.465 240.787 135.587C240.129 134.648 239.378 133.428 238.439 131.925C229.801 118.498 206.702 82.9112 172.054 82.9112C137.406 82.9112 169.425 82.9111 168.111 83.0989C138.909 85.3525 107.923 118.874 91.3971 136.808C86.1389 142.536 81.5379 147.418 80.505 147.418C78.0637 145.822 61.444 104.507 51.491 78.2163H65.106L84.9182 138.874L85.3877 138.31C85.5755 138.029 108.298 110.423 134.402 90.5168C144.637 82.7233 157.313 78.5919 171.209 78.5919C185.106 78.5919 204.073 84.5074 213.932 93.6154C229.989 108.545 259.472 148.733 259.754 149.108L260.317 149.953L275.81 78.498H292.618C289.237 80.0003 282.759 83.0989 281.726 86.3853C281.35 87.4182 280.505 91.6435 279.097 98.0285C274.965 117.09 266.327 156.808 262.007 158.78C261.632 158.967 261.162 159.061 260.787 159.061L260.505 158.967Z"
fill="#D5C0FC"
/>
<path
d="M13.2748 78.028L65.2935 77.8403C65.2935 77.8403 61.7255 68.3567 67.0776 53.1454C70.5518 43.2863 64.8241 39.906 61.256 41.1266C57.7818 42.3473 53.5565 48.3567 53.5565 48.3567C53.5565 48.3567 47.641 44.9764 43.6973 40.3755C39.7536 35.8684 38.9086 33.0515 36.0917 33.2393C33.2748 33.4271 31.5846 35.1172 32.3358 38.028C32.3358 38.028 30.1762 34.9294 26.2325 34.7417C22.2889 34.5539 22.3827 39.1548 22.3827 39.1548C22.3827 39.1548 16.5611 35.0233 12.7114 36.5257C8.86162 38.028 10.364 42.629 10.364 42.629C10.364 42.629 6.98369 38.028 3.41561 39.5304C-0.0585646 41.0327 -1.8426 48.075 2.94613 55.305C7.73486 62.5351 11.4907 68.9201 13.1809 78.2158L13.2748 78.028Z"
fill="#F48282"
/>
<mask
id="mask2_912_14740"
style={{ maskType: "luminance" }}
maskUnits="userSpaceOnUse"
x="0"
y="33"
width="69"
height="46"
>
<path
d="M13.2748 78.028L65.2935 77.8403C65.2935 77.8403 61.7255 68.3567 67.0776 53.1454C70.5518 43.2863 64.8241 39.906 61.256 41.1266C57.7818 42.3473 53.5565 48.3567 53.5565 48.3567C53.5565 48.3567 47.641 44.9764 43.6973 40.3755C39.7536 35.8684 38.9086 33.0515 36.0917 33.2393C33.3687 33.4271 31.5846 35.1172 32.3358 37.9341C32.3358 37.9341 30.1762 34.8356 26.2325 34.6478C22.3828 34.46 22.3827 39.0609 22.3827 39.0609C22.3827 39.0609 16.5611 34.9294 12.7114 36.4318C8.86162 37.9341 10.364 42.5351 10.364 42.5351C10.364 42.5351 6.98369 37.9341 3.41561 39.4365C-0.0585646 40.9388 -1.8426 47.9811 2.94613 55.2111C7.73486 62.4412 11.4907 68.8262 13.1809 78.1219L13.2748 78.028Z"
fill="white"
/>
</mask>
<g mask="url(#mask2_912_14740)">
<path
d="M33.5564 33.2389C36.1856 33.2389 39.5658 39.6239 42.6644 43.4737C45.763 47.3234 48.1104 49.1075 47.641 50.7976C47.1715 52.4878 45.763 53.5206 45.763 53.5206C45.763 53.5206 54.0259 50.6098 53.5564 47.0418C53.087 43.4737 59.0964 38.3094 59.0964 38.3094C59.0964 38.3094 66.2325 40.8446 65.1057 45.915C63.979 50.9854 59.2842 56.2436 59.0964 63.8493C58.9086 71.4549 59.1903 77.5582 55.7161 80.2812C52.2419 82.9103 70.364 80.7507 70.364 80.7507L73.3687 59.8117L70.7395 34.5535L37.8757 26.5723L33.3687 33.3328L33.5564 33.2389Z"
fill="#2973E8"
/>
<path
d="M24.1669 34.2717C26.3265 34.6473 26.9838 36.1496 29.519 40.1872C32.0542 44.2247 34.965 48.0745 37.0307 49.3891C39.0965 50.7036 40.411 48.6379 39.2843 47.4172C38.1575 46.1966 35.6223 43.2858 34.4016 41.3139C33.181 39.3421 31.303 35.3984 31.303 35.3984L26.6082 31.6426L24.073 34.2717H24.1669Z"
fill="#2973E8"
/>
<path
d="M12.2423 36.0556C14.3081 36.0556 16.0921 38.3091 19.3785 42.5345C22.6649 46.7598 25.3879 49.7645 26.9841 50.7974C28.6743 51.8303 32.0545 51.3608 30.1766 49.2012C28.2048 47.0415 25.7635 44.4124 24.5428 42.5345C23.3222 40.6566 21.7259 38.0274 21.7259 38.0274L18.5334 33.896L12.1484 36.1495L12.2423 36.0556Z"
fill="#2973E8"
/>
<path
d="M2.5708 39.0605C5.01212 39.6239 5.01211 39.3422 7.54732 44.1309C10.0825 48.9197 12.6177 52.2999 15.0591 53.6145C17.5004 54.9291 21.3501 54.3657 18.2515 52.1121C15.153 49.8586 13.3689 47.2295 12.5239 45.8211C11.5849 44.4126 9.23747 39.3422 9.23747 39.3422L5.10601 36.9009L2.6647 38.9666L2.5708 39.0605Z"
fill="#2973E8"
/>
<path
d="M59.754 67.5112C59.5662 68.9197 59.1906 70.0464 58.7211 70.9854C56.3737 72.8633 53.369 73.7084 50.4582 74.3657C49.8948 74.4596 50.0826 75.3986 50.7399 75.3047C52.9934 74.8352 55.247 74.2718 57.2188 73.2389C54.4958 75.7741 49.1437 76.807 37.8761 78.4032C20.693 80.8446 60.1296 82.0652 60.1296 82.0652L60.9747 70.6098L59.754 67.699V67.5112Z"
fill="#2973E8"
/>
<path
d="M48.1105 74.4595C47.5471 74.4595 47.5471 75.3984 48.1105 75.3984C48.6739 75.3984 48.6739 74.4595 48.1105 74.4595Z"
fill="#2973E8"
/>
</g>
<path
d="M13.2748 78.31H13.087C13.087 78.31 13.087 78.2161 13.087 78.1222C11.303 68.7325 7.45323 62.2537 2.94619 55.3053C0.317083 51.2678 -0.621889 46.7607 0.410975 43.1926C0.974356 41.2208 2.10112 39.8123 3.50957 39.249C6.23257 38.1222 8.86168 40.2818 10.0823 41.5964C9.89455 40.094 10.0823 37.371 12.8053 36.3382C16.1856 35.0236 20.8805 37.8405 22.2889 38.7795C22.2889 38.0283 22.6645 36.526 23.6974 35.4931C24.4485 34.8358 25.2936 34.5541 26.4204 34.5541C29.1434 34.648 31.1152 36.2443 32.0542 37.1832C32.0542 36.2443 32.242 35.4931 32.7114 34.8358C33.4626 33.803 34.6833 33.2396 36.2795 33.1457C38.3453 32.9579 39.472 34.4602 41.2561 36.8077C42.0072 37.8405 42.9462 38.9673 44.073 40.3757C47.5471 44.3194 52.5237 47.5119 53.6504 48.1692C54.4016 47.0424 58.0636 42.1598 61.3499 41.033C63.134 40.3757 65.1997 40.9391 66.7021 42.3476C68.0166 43.6621 69.8946 46.8546 67.5471 53.3335C62.195 68.2631 65.6692 77.7466 65.7631 77.8405C65.7631 77.8405 65.7631 78.0283 65.7631 78.1222C65.7631 78.2161 65.6692 78.2161 65.5753 78.2161L13.5565 78.4039L13.2748 78.31ZM5.01192 39.3429C4.54243 39.3429 4.16684 39.3429 3.69736 39.6246C2.4767 40.1879 1.44384 41.5025 0.880459 43.2865C-0.152405 46.7607 0.786561 51.08 3.32177 54.9297C7.82881 61.7842 11.7725 68.2631 13.5565 77.7466L64.918 77.5588C64.3546 75.587 62.1011 66.479 66.8898 53.0518C69.1434 46.8546 67.3593 43.8499 66.2326 42.6293C64.918 41.3147 63.134 40.8452 61.5377 41.4086C58.1575 42.6293 54.026 48.4509 54.026 48.5448C54.026 48.6386 53.7443 48.7325 53.6504 48.5448C53.6504 48.5448 47.641 45.0706 43.6974 40.5635C42.4767 39.1551 41.5377 37.9344 40.8805 36.9955C39.0964 34.648 38.1574 33.4274 36.3734 33.5213C34.965 33.6152 33.8382 34.1785 33.1809 35.0236C32.6175 35.7748 32.5237 36.8077 32.8053 37.9344C32.8053 38.0283 32.8053 38.2161 32.6175 38.2161C32.5236 38.2161 32.3359 38.2161 32.242 38.1222C32.242 38.1222 30.0823 35.1175 26.4204 34.9297C25.4814 34.9297 24.7302 35.1175 24.073 35.6809C22.8523 36.9015 22.7584 39.0612 22.7584 39.0612C22.7584 39.1551 22.7584 39.249 22.5706 39.3429C22.4767 39.3429 22.3828 39.3429 22.2889 39.3429C22.2889 39.3429 16.5612 35.3053 12.8992 36.7138C9.33117 38.1222 10.6457 42.2537 10.7396 42.4415C10.7396 42.5354 10.7396 42.7231 10.6457 42.817C10.5518 42.817 10.364 42.817 10.2701 42.817C10.2701 42.817 7.82882 39.5307 5.01192 39.5307V39.3429Z"
fill="#101720"
/>
<path
d="M38.5332 55.7746C38.4393 55.7746 38.2515 55.6807 38.2515 55.5868C38.2515 55.399 38.2515 55.3051 38.4393 55.3051C38.5332 55.3051 50.458 53.2393 53.2749 48.1689C53.2749 48.075 53.5566 47.9811 53.6505 48.075C53.7444 48.075 53.8383 48.3567 53.7444 48.4506C50.7397 53.7088 39.0027 55.7746 38.4393 55.7746H38.5332Z"
fill="#101720"
/>
<path
d="M40.2234 48.5447H40.0356C40.0356 48.5447 35.2469 43.9438 32.2422 38.1222C32.2422 38.0283 32.2422 37.8405 32.3361 37.7466C32.43 37.7466 32.6178 37.7466 32.7117 37.8405C35.7164 43.4743 40.4112 48.0752 40.4112 48.0752C40.5051 48.1691 40.5051 48.3569 40.4112 48.4508C40.4112 48.4508 40.3173 48.4508 40.2234 48.4508V48.5447Z"
fill="#101720"
/>
<path
d="M30.9276 50.2349H30.7398C30.7398 50.2349 24.6365 44.3194 22.2891 39.3429C22.2891 39.249 22.2891 39.0612 22.383 38.9673C22.4769 38.9673 22.6646 38.9673 22.7585 39.0612C25.106 43.9438 31.0215 49.7654 31.1154 49.8593C31.2093 49.9532 31.2093 50.141 31.1154 50.2349C31.1154 50.2349 31.0215 50.2349 30.9276 50.2349Z"
fill="#101720"
/>
<path
d="M19.6601 53.521C19.6601 53.521 19.5662 53.521 19.4723 53.521C19.4723 53.521 13.2751 49.1079 10.1765 42.629C10.1765 42.5351 10.1765 42.3473 10.2704 42.2534C10.3643 42.2534 10.5521 42.2534 10.646 42.3473C13.6507 48.6384 19.6601 52.9577 19.754 52.9577C19.8479 52.9577 19.9418 53.2393 19.754 53.3332L19.5662 53.4271L19.6601 53.521Z"
fill="#101720"
/>
<path
d="M327.547 78.028L275.528 77.8403C275.528 77.8403 279.096 68.3567 273.744 53.1454C270.27 43.2863 275.998 39.906 279.566 41.1266C283.04 42.3473 287.265 48.3567 287.265 48.3567C287.265 48.3567 293.181 44.9764 297.124 40.3755C301.068 35.8684 301.913 33.0515 304.73 33.2393C307.547 33.4271 309.237 35.1172 308.486 38.028C308.486 38.028 310.646 34.9294 314.589 34.7417C318.439 34.5539 318.439 39.1548 318.439 39.1548C318.439 39.1548 324.261 35.0233 328.11 36.5257C331.96 38.028 330.458 42.629 330.458 42.629C330.458 42.629 333.838 38.028 337.406 39.5304C340.88 41.0327 342.57 48.075 337.876 55.305C333.181 62.5351 329.331 68.9201 327.641 78.2158L327.547 78.028Z"
fill="#F48282"
/>
<mask
id="mask3_912_14740"
style={{ maskType: "luminance" }}
maskUnits="userSpaceOnUse"
x="272"
y="33"
width="69"
height="46"
>
<path
d="M327.547 78.028L275.528 77.8403C275.528 77.8403 279.096 68.3567 273.744 53.1454C270.27 43.2863 275.998 39.906 279.566 41.1266C283.04 42.3473 287.265 48.3567 287.265 48.3567C287.265 48.3567 293.181 44.9764 297.124 40.3755C301.068 35.8684 301.913 33.0515 304.73 33.2393C307.453 33.4271 309.237 35.1172 308.486 37.9341C308.486 37.9341 310.646 34.8356 314.589 34.6478C318.439 34.46 318.439 39.0609 318.439 39.0609C318.439 39.0609 324.261 34.9294 328.11 36.4318C331.96 37.9341 330.458 42.5351 330.458 42.5351C330.458 42.5351 333.838 37.9341 337.406 39.4365C340.88 40.9388 342.57 47.9811 337.876 55.2111C333.181 62.4412 329.331 68.8262 327.641 78.1219L327.547 78.028Z"
fill="white"
/>
</mask>
<g mask="url(#mask3_912_14740)">
<path
d="M307.172 33.2389C304.542 33.2389 301.162 39.6239 298.064 43.4737C294.965 47.3234 292.618 49.1075 293.087 50.7976C293.557 52.4878 294.965 53.5206 294.965 53.5206C294.965 53.5206 286.702 50.6098 287.172 47.0418C287.641 43.4737 281.632 38.3094 281.632 38.3094C281.632 38.3094 274.496 40.8446 275.622 45.915C276.749 50.9854 281.444 56.2436 281.632 63.8493C281.819 71.4549 281.538 77.5582 285.012 80.2812C288.486 82.9103 270.364 80.7507 270.364 80.7507L267.359 59.8117L269.989 34.5535L302.852 26.5723L307.359 33.3328L307.172 33.2389Z"
fill="#2973E8"
/>
<path
d="M316.561 34.2717C314.401 34.6473 313.744 36.1496 311.115 40.1872C308.58 44.2247 305.669 48.0745 303.603 49.3891C301.538 50.7036 300.223 48.6379 301.35 47.4172C302.477 46.1966 305.012 43.2858 306.232 41.3139C307.453 39.3421 309.331 35.3984 309.331 35.3984L314.026 31.6426L316.561 34.2717Z"
fill="#2973E8"
/>
<path
d="M328.58 36.0556C326.514 36.0556 324.73 38.3091 321.444 42.5345C318.064 46.7598 315.435 49.7645 313.838 50.7974C312.242 51.8303 308.674 51.3608 310.646 49.2012C312.618 47.0415 315.059 44.4124 316.28 42.5345C317.5 40.6566 319.097 38.0274 319.097 38.0274L322.289 33.896L328.674 36.1495L328.58 36.0556Z"
fill="#2973E8"
/>
<path
d="M338.251 39.0605C335.81 39.6239 335.81 39.3422 333.275 44.1309C330.74 48.9197 328.204 52.2999 325.763 53.6145C323.322 54.9291 319.472 54.3657 322.571 52.1121C325.669 49.8586 327.453 47.2295 328.298 45.8211C329.143 44.4126 331.585 39.3422 331.585 39.3422L335.716 36.9009L338.157 38.9666L338.251 39.0605Z"
fill="#2973E8"
/>
<path
d="M280.974 67.5112C281.162 68.9197 281.444 70.0464 281.913 70.9854C284.261 72.8633 287.266 73.7084 290.176 74.3657C290.74 74.4596 290.552 75.3986 289.895 75.3047C287.641 74.8352 285.388 74.2718 283.416 73.2389C286.139 75.7741 291.491 76.807 302.759 78.4032C319.942 80.8446 280.505 82.0652 280.505 82.0652L279.66 70.6098L280.881 67.699L280.974 67.5112Z"
fill="#2973E8"
/>
<path
d="M292.618 74.4595C293.181 74.4595 293.181 75.3984 292.618 75.3984C292.054 75.3984 292.054 74.4595 292.618 74.4595Z"
fill="#2973E8"
/>
</g>
<path
d="M327.547 78.3097L275.528 78.1219C275.528 78.1219 275.34 78.1219 275.34 78.028C275.34 77.9341 275.34 77.8402 275.34 77.7463C275.34 77.6524 278.815 68.1688 273.556 53.2392C271.209 46.6665 273.087 43.5679 274.401 42.2533C275.81 40.8449 277.97 40.2815 279.754 40.9388C282.946 42.0655 286.608 46.9482 287.453 48.0749C288.58 47.4176 293.556 44.319 297.031 40.2815C298.251 38.9669 299.096 37.7463 299.848 36.7134C301.632 34.366 302.758 32.8636 304.824 33.0514C306.42 33.1453 307.735 33.8026 308.392 34.7416C308.862 35.3988 309.049 36.2439 309.049 37.089C309.988 36.15 311.866 34.5538 314.683 34.4599C315.81 34.4599 316.749 34.7416 317.406 35.3989C318.439 36.4317 318.721 37.9341 318.815 38.6852C320.223 37.7463 324.918 34.9294 328.298 36.2439C331.115 37.3707 331.303 39.9998 331.021 41.5021C332.242 40.1876 334.871 37.9341 337.594 39.1547C339.002 39.7181 340.129 41.2205 340.693 43.0984C341.725 46.6665 340.786 51.2674 338.157 55.2111C333.65 62.0655 329.707 68.5444 328.017 78.028C328.017 78.028 328.017 78.1219 328.017 78.2158C328.017 78.2158 327.923 78.2158 327.829 78.2158L327.547 78.3097ZM275.904 77.5585L327.265 77.7463C329.049 68.2627 332.993 61.7838 337.5 54.9294C340.035 51.0796 340.974 46.6665 339.941 43.2862C339.378 41.5021 338.439 40.1876 337.125 39.6242C333.838 38.2158 330.552 42.6289 330.552 42.6289C330.552 42.7228 330.364 42.8167 330.176 42.6289C330.082 42.6289 329.988 42.4411 330.082 42.2533C330.082 42.0655 331.491 37.9341 327.923 36.5256C324.261 35.1172 318.627 39.1547 318.533 39.1547C318.533 39.1547 318.345 39.1547 318.251 39.1547C318.251 39.1547 318.063 38.9669 318.063 38.873C318.063 38.7791 318.063 36.7134 316.749 35.4927C316.186 34.9294 315.34 34.6477 314.401 34.7416C310.74 34.9294 308.58 37.9341 308.58 37.9341C308.58 38.028 308.392 38.1219 308.204 38.028C308.11 38.028 308.017 37.8402 308.017 37.7463C308.298 36.6195 308.204 35.5866 307.641 34.8355C306.984 33.9904 305.857 33.427 304.448 33.3331C302.664 33.2392 301.725 34.366 299.941 36.8073C299.19 37.7463 298.251 39.0608 297.125 40.3754C293.181 44.8824 287.265 48.3566 287.171 48.3566C287.078 48.3566 286.89 48.3566 286.796 48.3566C286.796 48.3566 282.664 42.4411 279.284 41.2205C277.688 40.6571 275.81 41.2205 274.589 42.4411C273.369 43.6618 271.678 46.6665 273.838 52.8636C278.627 66.2909 276.373 75.4928 275.81 77.3707L275.904 77.5585Z"
fill="#101720"
/>
<path
d="M302.195 55.7747C301.725 55.7747 289.894 53.6151 286.89 48.4508C286.89 48.3569 286.89 48.1691 286.984 48.0752C287.077 48.0752 287.265 48.0752 287.359 48.1691C290.176 53.1456 302.101 55.2113 302.195 55.3052C302.383 55.3052 302.477 55.493 302.383 55.5869C302.383 55.6808 302.195 55.7747 302.101 55.7747H302.195Z"
fill="#101720"
/>
<path
d="M300.599 48.5449H300.411C300.317 48.451 300.317 48.2632 300.411 48.1693C300.411 48.1693 305.106 43.5684 308.111 37.9346C308.111 37.8407 308.392 37.7468 308.486 37.8406C308.58 37.8406 308.674 38.1223 308.58 38.2162C305.576 43.9439 300.787 48.5449 300.787 48.6388C300.787 48.6388 300.693 48.6388 300.599 48.6388V48.5449Z"
fill="#101720"
/>
<path
d="M309.894 50.2347H309.707C309.613 50.1408 309.613 49.953 309.707 49.8591C309.707 49.8591 315.716 44.0375 318.063 39.061C318.063 38.9671 318.345 38.8732 318.439 38.9671C318.533 38.9671 318.627 39.2488 318.533 39.3427C316.092 44.3192 310.082 50.2347 310.082 50.2347C310.082 50.2347 309.988 50.2347 309.894 50.2347Z"
fill="#101720"
/>
<path
d="M321.162 53.521C321.162 53.521 320.975 53.521 320.975 53.4271C320.975 53.3332 320.975 53.1454 320.975 53.0515C320.975 53.0515 327.078 48.7323 330.083 42.4412C330.083 42.2534 330.364 42.2534 330.458 42.3473C330.552 42.3473 330.646 42.629 330.552 42.7229C327.453 49.2018 321.35 53.521 321.256 53.6149C321.256 53.6149 321.162 53.6149 321.069 53.6149L321.162 53.521Z"
fill="#101720"
/>
</svg>
</div>
<div className="flex flex-col items-center gap-2">
<Text variant="h4" className="text-center text-[1.375rem]">
No triggers yet
</Text>
<Text variant="large" className="text-zinc-700">
Set up automatic triggers for your agent to run tasks automatically
they&apos;ll show up here.
</Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { cn } from "@/lib/utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
type Props = {
children: React.ReactNode;
};
export function AnchorLinksWrap({ children }: Props) {
return (
<div className={cn(AGENT_LIBRARY_SECTION_PADDING_X, "hidden lg:block")}>
<nav className="flex gap-8 px-3 pb-1">{children}</nav>
</div>
);
}

View File

@@ -166,7 +166,7 @@ function renderMarkdown(
className="prose prose-sm dark:prose-invert max-w-none"
remarkPlugins={[
remarkGfm, // GitHub Flavored Markdown (tables, task lists, strikethrough)
remarkMath, // Math support for LaTeX
[remarkMath, { singleDollarTextMath: false }], // Math support for LaTeX
]}
rehypePlugins={[
rehypeKatex, // Render math with KaTeX

View File

@@ -1,6 +1,7 @@
import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
import moment from "moment";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
@@ -20,7 +21,20 @@ export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
<div className="flex w-full flex-col gap-0">
<div className="flex w-full flex-col flex-wrap items-start justify-between gap-1 md:flex-row md:items-center">
<div className="flex min-w-0 flex-1 flex-col items-start gap-3">
{run?.status ? <RunStatusBadge status={run.status} /> : null}
{run?.status ? (
<RunStatusBadge status={run.status} />
) : scheduleRecurrence ? (
<div className="inline-flex items-center gap-1 rounded-md bg-yellow-50 p-1">
<ClockClockwiseIcon
size={16}
className="text-yellow-700"
weight="bold"
/>
<Text variant="small-medium" className="text-yellow-700">
Scheduled
</Text>
</div>
) : null}
<Text variant="h2" className="truncate text-ellipsis">
{agent.name}
</Text>

View File

@@ -0,0 +1,11 @@
type Props = {
children: React.ReactNode;
};
export function SelectedActionsWrap({ children }: Props) {
return (
<div className="my-0 ml-4 flex flex-row items-center gap-3 lg:mx-0 lg:my-4 lg:flex-col">
{children}
</div>
);
}

View File

@@ -4,18 +4,18 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
ScrollableTabs,
ScrollableTabsContent,
ScrollableTabsList,
ScrollableTabsTrigger,
} from "@/components/molecules/ScrollableTabs/ScrollableTabs";
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
import { InfoIcon } from "@phosphor-icons/react";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useEffect } from "react";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
@@ -24,11 +24,9 @@ import { SelectedViewLayout } from "../SelectedViewLayout";
import { RunOutputs } from "./components/RunOutputs";
import { RunSummary } from "./components/RunSummary";
import { SelectedRunActions } from "./components/SelectedRunActions/SelectedRunActions";
import { WebhookTriggerSection } from "./components/WebhookTriggerSection";
import { useSelectedRunView } from "./useSelectedRunView";
const anchorStyles =
"border-b-2 border-transparent pb-1 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 hover:border-slate-900";
interface Props {
agent: LibraryAgent;
runId: string;
@@ -42,10 +40,11 @@ export function SelectedRunView({
onSelectRun,
onClearSelectedRun,
}: Props) {
const { run, isLoading, responseError, httpError } = useSelectedRunView(
agent.graph_id,
runId,
);
const { run, preset, isLoading, responseError, httpError } =
useSelectedRunView(agent.graph_id, runId);
const breakpoint = useBreakpoint();
const isLgScreenUp = isLargeScreen(breakpoint);
const {
pendingReviews,
@@ -62,13 +61,6 @@ export function SelectedRunView({
const withSummary = run?.stats?.activity_status;
const withReviews = run?.status === AgentExecutionStatus.REVIEW;
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
if (responseError || httpError) {
return (
<ErrorCard
@@ -90,131 +82,148 @@ export function SelectedRunView({
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={run} />
{/* Navigation Links */}
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<nav className="flex gap-8 px-3 pb-1">
{withSummary && (
<button
onClick={() => scrollToSection("summary")}
className={anchorStyles}
>
Summary
</button>
)}
<button
onClick={() => scrollToSection("output")}
className={anchorStyles}
>
Output
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
{withReviews && (
<button
onClick={() => scrollToSection("reviews")}
className={anchorStyles}
>
Reviews ({pendingReviews.length})
</button>
)}
</nav>
</div>
{!isLgScreenUp ? (
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
) : null}
{/* Summary Section */}
{withSummary && (
<div id="summary" className="scroll-mt-4">
<RunDetailCard
title={
<div className="flex items-center gap-2">
<Text variant="lead-semibold">Summary</Text>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={8}
className="cursor-help text-neutral-500 hover:text-neutral-700"
/>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
This AI-generated summary describes how the agent
handled your task. It&apos;s an experimental
feature and may occasionally be inaccurate.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<RunSummary run={run} />
</RunDetailCard>
</div>
)}
{/* Output Section */}
<div id="output" className="scroll-mt-4">
<RunDetailCard title="Output">
{isLoading ? (
<div className="text-neutral-500">
<LoadingSpinner />
</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<Text variant="body" className="text-neutral-600">
No output from this run.
</Text>
)}
</RunDetailCard>
</div>
{/* Input Section */}
<div id="input" className="scroll-mt-4">
<RunDetailCard title="Your input">
<AgentInputsReadOnly
agent={agent}
inputs={(run as any)?.inputs}
credentialInputs={(run as any)?.credential_inputs}
{preset &&
agent.trigger_setup_info &&
preset.webhook_id &&
preset.webhook && (
<WebhookTriggerSection
preset={preset}
triggerSetupInfo={agent.trigger_setup_info}
/>
</RunDetailCard>
</div>
)}
{/* Reviews Section */}
{withReviews && (
<div id="reviews" className="scroll-mt-4">
<RunDetailCard>
{reviewsLoading ? (
<div className="text-neutral-500">Loading reviews</div>
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
/>
) : (
<div className="text-neutral-600">
No pending reviews for this execution
<ScrollableTabs
defaultValue="output"
className="-mt-2 flex flex-col"
>
<ScrollableTabsList className="px-4">
{withSummary && (
<ScrollableTabsTrigger value="summary">
Summary
</ScrollableTabsTrigger>
)}
<ScrollableTabsTrigger value="output">
Output
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="input">
Your input
</ScrollableTabsTrigger>
{withReviews && (
<ScrollableTabsTrigger value="reviews">
Reviews ({pendingReviews.length})
</ScrollableTabsTrigger>
)}
</ScrollableTabsList>
<div className="my-6 flex flex-col gap-6">
{/* Summary Section */}
{withSummary && (
<ScrollableTabsContent value="summary">
<div className="scroll-mt-4">
<RunDetailCard
title={
<div className="flex items-center gap-1">
<Text variant="lead-semibold">Summary</Text>
<InformationTooltip
iconSize={20}
description="This AI-generated summary describes how the agent handled your task. It's an experimental feature and may occasionally be inaccurate."
/>
</div>
}
>
<RunSummary run={run} />
</RunDetailCard>
</div>
)}
</RunDetailCard>
</ScrollableTabsContent>
)}
{/* Output Section */}
<ScrollableTabsContent value="output">
<div className="scroll-mt-4">
<RunDetailCard title="Output">
{isLoading ? (
<div className="text-neutral-500">
<LoadingSpinner />
</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<Text variant="body" className="text-neutral-600">
No output from this run.
</Text>
)}
</RunDetailCard>
</div>
</ScrollableTabsContent>
{/* Input Section */}
<ScrollableTabsContent value="input">
<div id="input" className="scroll-mt-4">
<RunDetailCard
title={
<div className="flex items-center gap-1">
<Text variant="lead-semibold">Your input</Text>
<InformationTooltip
iconSize={20}
description="This is the input that was provided to the agent for running this task."
/>
</div>
}
>
<AgentInputsReadOnly
agent={agent}
inputs={run?.inputs}
credentialInputs={run?.credential_inputs}
/>
</RunDetailCard>
</div>
</ScrollableTabsContent>
{/* Reviews Section */}
{withReviews && (
<ScrollableTabsContent value="reviews">
<div className="scroll-mt-4">
<RunDetailCard>
{reviewsLoading ? (
<LoadingSpinner size="small" />
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
/>
) : (
<Text variant="body" className="text-zinc-700">
No pending reviews for this execution
</Text>
)}
</RunDetailCard>
</div>
</ScrollableTabsContent>
)}
</div>
)}
</ScrollableTabs>
</div>
</SelectedViewLayout>
</div>
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
</div>
{isLgScreenUp ? (
<div className="max-w-[3.75rem] flex-shrink-0">
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,98 @@
"use client";
import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useState } from "react";
interface Props {
isOpen: boolean;
onClose: () => void;
onCreate: (name: string, description: string) => Promise<void>;
run?: GraphExecution;
}
export function CreateTemplateModal({ isOpen, onClose, onCreate }: Props) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [isCreating, setIsCreating] = useState(false);
async function handleSubmit() {
if (!name.trim()) return;
setIsCreating(true);
try {
await onCreate(name.trim(), description.trim());
setName("");
setDescription("");
onClose();
} finally {
setIsCreating(false);
}
}
function handleCancel() {
setName("");
setDescription("");
onClose();
}
return (
<Dialog
controlled={{ isOpen, set: () => onClose() }}
styling={{ maxWidth: "500px" }}
>
<Dialog.Content>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<Text variant="lead" as="h2" className="!font-medium !text-black">
Create Template
</Text>
<Text variant="body" className="text-zinc-600">
Save this task as a template to reuse later with the same inputs
and credentials.
</Text>
</div>
<div className="flex w-[96%] flex-col gap-4 pl-1">
<Input
id="template-name"
label="Name"
placeholder="Enter template name"
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
/>
<Input
type="textarea"
id="template-description"
label="Description"
placeholder="Optional description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
/>
</div>
</div>
<Dialog.Footer className="mt-6">
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={handleCancel}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={!name.trim() || isCreating}
loading={isCreating}
>
Create Template
</Button>
</div>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -7,44 +7,55 @@ import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import {
ArrowBendLeftUpIcon,
ArrowBendRightDownIcon,
CardsThreeIcon,
EyeIcon,
StopIcon,
} from "@phosphor-icons/react";
import { AgentActionsDropdown } from "../../../AgentActionsDropdown";
import { SelectedActionsWrap } from "../../../SelectedActionsWrap";
import { ShareRunButton } from "../../../ShareRunButton/ShareRunButton";
import { CreateTemplateModal } from "../CreateTemplateModal/CreateTemplateModal";
import { useSelectedRunActions } from "./useSelectedRunActions";
type Props = {
agent: LibraryAgent;
run: GraphExecution | undefined;
scheduleRecurrence?: string;
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
};
export function SelectedRunActions(props: Props) {
export function SelectedRunActions({
agent,
run,
onSelectRun,
onClearSelectedRun,
}: Props) {
const {
canRunManually,
handleRunAgain,
handleStopRun,
isRunningAgain,
canStop,
isStopping,
openInBuilderHref,
handleCreateTemplate,
isCreateTemplateModalOpen,
setIsCreateTemplateModalOpen,
} = useSelectedRunActions({
agentGraphId: props.agent.graph_id,
run: props.run,
onSelectRun: props.onSelectRun,
onClearSelectedRun: props.onClearSelectedRun,
agentGraphId: agent.graph_id,
run: run,
agent: agent,
onSelectRun: onSelectRun,
});
const shareExecutionResultsEnabled = useGetFlag(Flag.SHARE_EXECUTION_RESULTS);
const isRunning = props.run?.status === "RUNNING";
const isRunning = run?.status === "RUNNING";
if (!props.run || !props.agent) return null;
if (!run || !agent) return null;
return (
<div className="my-4 flex flex-col items-center gap-3">
{!isRunning ? (
<SelectedActionsWrap>
{canRunManually && !isRunning ? (
<Button
variant="icon"
size="icon"
@@ -96,23 +107,38 @@ export function SelectedRunActions(props: Props) {
) : null}
{shareExecutionResultsEnabled && (
<ShareRunButton
graphId={props.agent.graph_id}
executionId={props.run.id}
isShared={props.run.is_shared}
shareToken={props.run.share_token}
graphId={agent.graph_id}
executionId={run.id}
isShared={run.is_shared}
shareToken={run.share_token}
/>
)}
<FloatingSafeModeToggle
graph={props.agent}
variant="white"
fullWidth={false}
/>
<FloatingSafeModeToggle graph={agent} variant="white" fullWidth={false} />
{canRunManually && (
<>
<Button
variant="icon"
size="icon"
aria-label="Save task as template"
onClick={() => setIsCreateTemplateModalOpen(true)}
title="Create template"
>
<CardsThreeIcon weight="bold" size={18} className="text-zinc-700" />
</Button>
<CreateTemplateModal
isOpen={isCreateTemplateModalOpen}
onClose={() => setIsCreateTemplateModalOpen(false)}
onCreate={handleCreateTemplate}
run={run}
/>
</>
)}
<AgentActionsDropdown
agent={props.agent}
run={props.run}
agentGraphId={props.agent.graph_id}
onClearSelectedRun={props.onClearSelectedRun}
agent={agent}
run={run}
agentGraphId={agent.graph_id}
onClearSelectedRun={onClearSelectedRun}
/>
</div>
</SelectedActionsWrap>
);
}

View File

@@ -5,26 +5,39 @@ import {
usePostV1ExecuteGraphAgent,
usePostV1StopGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2ListPresetsQueryKey,
usePostV2CreateANewPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Args {
interface Params {
agentGraphId: string;
run?: GraphExecution;
agent?: LibraryAgent;
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
}
export function useSelectedRunActions(args: Args) {
export function useSelectedRunActions({
agentGraphId,
run,
agent,
onSelectRun,
}: Params) {
const queryClient = useQueryClient();
const { toast } = useToast();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isCreateTemplateModalOpen, setIsCreateTemplateModalOpen] =
useState(false);
const canStop =
args.run?.status === "RUNNING" || args.run?.status === "QUEUED";
const canStop = run?.status === "RUNNING" || run?.status === "QUEUED";
const canRunManually = !agent?.trigger_setup_info;
const { mutateAsync: stopRun, isPending: isStopping } =
usePostV1StopGraphExecution();
@@ -32,19 +45,22 @@ export function useSelectedRunActions(args: Args) {
const { mutateAsync: executeRun, isPending: isRunningAgain } =
usePostV1ExecuteGraphAgent();
const { mutateAsync: createPreset, isPending: isCreatingTemplate } =
usePostV2CreateANewPreset();
async function handleStopRun() {
try {
await stopRun({
graphId: args.run?.graph_id ?? "",
graphExecId: args.run?.id ?? "",
graphId: run?.graph_id ?? "",
graphExecId: run?.id ?? "",
});
toast({ title: "Run stopped" });
await queryClient.invalidateQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
args.agentGraphId,
).queryKey,
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
});
} catch (error: unknown) {
toast({
@@ -59,7 +75,7 @@ export function useSelectedRunActions(args: Args) {
}
async function handleRunAgain() {
if (!args.run) {
if (!run) {
toast({
title: "Run not found",
description: "Run not found",
@@ -72,11 +88,11 @@ export function useSelectedRunActions(args: Args) {
toast({ title: "Run started" });
const res = await executeRun({
graphId: args.run.graph_id,
graphVersion: args.run.graph_version,
graphId: run.graph_id,
graphVersion: run.graph_version,
data: {
inputs: args.run.inputs || {},
credentials_inputs: args.run.credential_inputs || {},
inputs: run.inputs || {},
credentials_inputs: run.credential_inputs || {},
source: "library",
},
});
@@ -84,12 +100,12 @@ export function useSelectedRunActions(args: Args) {
const newRunId = res?.status === 200 ? (res?.data?.id ?? "") : "";
await queryClient.invalidateQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
args.agentGraphId,
).queryKey,
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
});
if (newRunId && args.onSelectRun) args.onSelectRun(newRunId);
if (newRunId && onSelectRun) onSelectRun(newRunId);
} catch (error: unknown) {
toast({
title: "Failed to start run",
@@ -106,9 +122,55 @@ export function useSelectedRunActions(args: Args) {
setShowDeleteDialog(open);
}
async function handleCreateTemplate(name: string, description: string) {
if (!run) {
toast({
title: "Run not found",
description: "Cannot create template from missing run",
variant: "destructive",
});
return;
}
try {
const res = await createPreset({
data: {
name,
description,
graph_execution_id: run.id,
},
});
if (res.status === 200) {
toast({
title: "Template created",
});
if (agent) {
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
}
setIsCreateTemplateModalOpen(false);
}
} catch (error: unknown) {
toast({
title: "Failed to create template",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
// Open in builder URL helper
const openInBuilderHref = args.run
? `/build?flowID=${args.run.graph_id}&flowVersion=${args.run.graph_version}&flowExecutionID=${args.run.id}`
const openInBuilderHref = run
? `/build?flowID=${run.graph_id}&flowVersion=${run.graph_version}&flowExecutionID=${run.id}`
: undefined;
return {
@@ -116,9 +178,14 @@ export function useSelectedRunActions(args: Args) {
showDeleteDialog,
canStop,
isStopping,
canRunManually,
isRunningAgain,
handleShowDeleteDialog,
handleStopRun,
handleRunAgain,
handleCreateTemplate,
isCreatingTemplate,
isCreateTemplateModalOpen,
setIsCreateTemplateModalOpen,
} as const;
}

View File

@@ -0,0 +1,92 @@
"use client";
import { GraphTriggerInfo } from "@/app/api/__generated__/models/graphTriggerInfo";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { CopyIcon } from "@phosphor-icons/react";
import { RunDetailCard } from "../../RunDetailCard/RunDetailCard";
interface Props {
preset: LibraryAgentPreset;
triggerSetupInfo: GraphTriggerInfo;
}
function getTriggerStatus(
preset: LibraryAgentPreset,
): "active" | "inactive" | "broken" {
if (!preset.webhook_id || !preset.webhook) return "broken";
return preset.is_active ? "active" : "inactive";
}
export function WebhookTriggerSection({ preset, triggerSetupInfo }: Props) {
const status = getTriggerStatus(preset);
const webhook = preset.webhook;
function handleCopyWebhookUrl() {
if (webhook?.url) {
navigator.clipboard.writeText(webhook.url);
}
}
return (
<RunDetailCard title="Trigger Status">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Text variant="large-medium">Status</Text>
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
status === "active"
? "bg-green-100 text-green-800"
: status === "inactive"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{status === "active"
? "Active"
: status === "inactive"
? "Inactive"
: "Broken"}
</span>
</div>
{!preset.webhook_id ? (
<Text variant="body" className="text-red-600">
This trigger is not attached to a webhook. Use &quot;Set up
trigger&quot; to fix this.
</Text>
) : !triggerSetupInfo.credentials_input_name && webhook ? (
<div className="flex flex-col gap-2">
<Text variant="body">
This trigger is ready to be used. Use the Webhook URL below to set
up the trigger connection with the service of your choosing.
</Text>
<div className="flex flex-col gap-1">
<Text variant="small-medium">Webhook URL:</Text>
<div className="flex gap-2 rounded-md bg-gray-50 p-2">
<code className="flex-1 select-all text-sm">{webhook.url}</code>
<Button
variant="outline"
size="icon"
className="size-7 flex-none p-1"
onClick={handleCopyWebhookUrl}
title="Copy webhook URL"
>
<CopyIcon className="size-4" />
</Button>
</div>
</div>
</div>
) : (
<Text variant="body" className="text-muted-foreground">
This agent trigger is{" "}
{preset.is_active
? "ready. When a trigger is received, it will run with the provided settings."
: "disabled. It will not respond to triggers until you enable it."}
</Text>
)}
</div>
</RunDetailCard>
);
}

View File

@@ -1,8 +1,11 @@
"use client";
import { useGetV1GetExecutionDetails } from "@/app/api/__generated__/endpoints/graphs/graphs";
import type { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
import { useGetV2GetASpecificPreset } from "@/app/api/__generated__/endpoints/presets/presets";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import type { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { okData } from "@/app/api/helpers";
export function useSelectedRunView(graphId: string, runId: string) {
const query = useGetV1GetExecutionDetails(graphId, runId, {
@@ -37,6 +40,18 @@ export function useSelectedRunView(graphId: string, runId: string) {
? (query.data?.data as GetV1GetExecutionDetails200)
: undefined;
const presetId =
run && "preset_id" in run && run.preset_id
? (run.preset_id as string)
: undefined;
const presetQuery = useGetV2GetASpecificPreset(presetId || "", {
query: {
enabled: !!presetId,
select: (res) => okData<LibraryAgentPreset>(res),
},
});
const httpError =
status && status !== 200
? { status, statusText: `Request failed: ${status}` }
@@ -44,8 +59,9 @@ export function useSelectedRunView(graphId: string, runId: string) {
return {
run,
isLoading: query.isLoading,
responseError: query.error,
preset: presetQuery.data,
isLoading: query.isLoading || presetQuery.isLoading,
responseError: query.error || presetQuery.error,
httpError,
} as const;
}

View File

@@ -6,8 +6,8 @@ import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
@@ -16,9 +16,6 @@ import { SelectedViewLayout } from "../SelectedViewLayout";
import { SelectedScheduleActions } from "./components/SelectedScheduleActions";
import { useSelectedScheduleView } from "./useSelectedScheduleView";
const anchorStyles =
"border-b-2 border-transparent pb-1 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 hover:border-slate-900";
interface Props {
agent: LibraryAgent;
scheduleId: string;
@@ -41,12 +38,8 @@ export function SelectedScheduleView({
},
});
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
const breakpoint = useBreakpoint();
const isLgScreenUp = isLargeScreen(breakpoint);
if (error) {
return (
@@ -83,38 +76,25 @@ export function SelectedScheduleView({
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<div className="flex flex-col gap-4">
<div>
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col gap-0">
<RunDetailHeader
<div className="flex w-full flex-col gap-0">
<RunDetailHeader
agent={agent}
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
: undefined
}
/>
{schedule && !isLgScreenUp ? (
<div className="mt-4">
<SelectedScheduleActions
agent={agent}
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
: undefined
}
scheduleId={schedule.id}
onDeleted={onClearSelectedRun}
/>
</div>
</div>
</div>
{/* Navigation Links */}
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<nav className="flex gap-8 px-3 pb-1">
<button
onClick={() => scrollToSection("schedule")}
className={anchorStyles}
>
Schedule
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
</nav>
) : null}
</div>
{/* Schedule Section */}
@@ -174,10 +154,6 @@ export function SelectedScheduleView({
<div id="input" className="scroll-mt-4">
<RunDetailCard title="Your input">
<div className="relative">
{/* {// TODO: re-enable edit inputs modal once the API supports it */}
{/* {schedule && Object.keys(schedule.input_data).length > 0 && (
<EditInputsModal agent={agent} schedule={schedule} />
)} */}
<AgentInputsReadOnly
agent={agent}
inputs={schedule?.input_data}
@@ -189,8 +165,8 @@ export function SelectedScheduleView({
</div>
</SelectedViewLayout>
</div>
{schedule ? (
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
{schedule && isLgScreenUp ? (
<div className="max-w-[3.75rem] flex-shrink-0">
<SelectedScheduleActions
agent={agent}
scheduleId={schedule.id}

View File

@@ -1,84 +0,0 @@
"use client";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { PencilSimpleIcon } from "@phosphor-icons/react";
import { RunAgentInputs } from "../../../../modals/RunAgentInputs/RunAgentInputs";
import { useEditInputsModal } from "./useEditInputsModal";
type Props = {
agent: LibraryAgent;
schedule: GraphExecutionJobInfo;
};
export function EditInputsModal({ agent, schedule }: Props) {
const {
isOpen,
setIsOpen,
inputFields,
values,
setValues,
handleSave,
isSaving,
} = useEditInputsModal(agent, schedule);
return (
<Dialog
controlled={{ isOpen, set: setIsOpen }}
styling={{ maxWidth: "32rem" }}
>
<Dialog.Trigger>
<Button
variant="ghost"
size="small"
className="absolute -right-2 -top-2"
>
<PencilSimpleIcon className="size-4" /> Edit inputs
</Button>
</Dialog.Trigger>
<Dialog.Content>
<div className="flex flex-col gap-4">
<Text variant="h3">Edit inputs</Text>
<div className="flex flex-col gap-4">
{Object.entries(inputFields).map(([key, fieldSchema]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">
{fieldSchema?.title || key}
</label>
<RunAgentInputs
schema={fieldSchema as any}
value={values[key]}
onChange={(v) => setValues((prev) => ({ ...prev, [key]: v }))}
/>
</div>
))}
</div>
</div>
<Dialog.Footer>
<div className="flex w-full justify-end gap-2">
<Button
variant="secondary"
size="small"
onClick={() => setIsOpen(false)}
className="min-w-32"
>
Cancel
</Button>
<Button
variant="primary"
size="small"
onClick={handleSave}
loading={isSaving}
className="min-w-32"
>
{isSaving ? "Saving…" : "Save"}
</Button>
</div>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,78 +0,0 @@
"use client";
import { useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getGetV1ListExecutionSchedulesForAGraphQueryKey } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { useToast } from "@/components/molecules/Toast/use-toast";
function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const schema = agent.input_schema as unknown as {
properties?: Record<string, any>;
} | null;
if (!schema || !schema.properties) return {};
const properties = schema.properties as Record<string, any>;
const visibleEntries = Object.entries(properties).filter(
([, sub]) => !sub?.hidden,
);
return Object.fromEntries(visibleEntries);
}
export function useEditInputsModal(
agent: LibraryAgent,
schedule: GraphExecutionJobInfo,
) {
const queryClient = useQueryClient();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const inputFields = useMemo(() => getAgentInputFields(agent), [agent]);
const [values, setValues] = useState<Record<string, any>>({
...(schedule.input_data as Record<string, any>),
});
async function handleSave() {
setIsSaving(true);
try {
const res = await fetch(`/api/schedules/${schedule.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ inputs: values }),
});
if (!res.ok) {
let message = "Failed to update schedule inputs";
const data = await res.json();
message = data?.message || data?.detail || message;
throw new Error(message);
}
await queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
schedule.graph_id,
),
});
toast({
title: "Schedule inputs updated",
});
setIsOpen(false);
} catch (error: any) {
toast({
title: "Failed to update schedule inputs",
description: error?.message || "An unexpected error occurred.",
variant: "destructive",
});
}
setIsSaving(false);
}
return {
isOpen,
setIsOpen,
inputFields,
values,
setValues,
handleSave,
isSaving,
} as const;
}

View File

@@ -3,6 +3,7 @@ import { Button } from "@/components/atoms/Button/Button";
import { EyeIcon } from "@phosphor-icons/react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
import { useScheduleDetailHeader } from "../../RunDetailHeader/useScheduleDetailHeader";
import { SelectedActionsWrap } from "../../SelectedActionsWrap";
type Props = {
agent: LibraryAgent;
@@ -19,20 +20,21 @@ export function SelectedScheduleActions({ agent, scheduleId }: Props) {
return (
<>
<div className="my-4 flex flex-col items-center gap-3">
<SelectedActionsWrap>
{openInBuilderHref && (
<Button
variant="icon"
size="icon"
aria-label="Open in builder"
as="NextLink"
href={openInBuilderHref}
target="_blank"
aria-label="View scheduled task details"
>
<EyeIcon weight="bold" size={18} className="text-zinc-700" />
</Button>
)}
<AgentActionsDropdown agent={agent} scheduleId={scheduleId} />
</div>
</SelectedActionsWrap>
</>
);
}

View File

@@ -0,0 +1,192 @@
"use client";
import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Input } from "@/components/atoms/Input/Input";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import {
getAgentCredentialsFields,
getAgentInputFields,
} from "../../modals/AgentInputsReadOnly/helpers";
import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInputs";
import { RunAgentInputs } from "../../modals/RunAgentInputs/RunAgentInputs";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
import { SelectedViewLayout } from "../SelectedViewLayout";
import { SelectedTemplateActions } from "./components/SelectedTemplateActions";
import { WebhookTriggerCard } from "./components/WebhookTriggerCard";
import { useSelectedTemplateView } from "./useSelectedTemplateView";
interface Props {
agent: LibraryAgent;
templateId: string;
onClearSelectedRun?: () => void;
onRunCreated?: (execution: GraphExecutionMeta) => void;
onSwitchToRunsTab?: () => void;
}
export function SelectedTemplateView({
agent,
templateId,
onClearSelectedRun,
onRunCreated,
onSwitchToRunsTab,
}: Props) {
const {
template,
isLoading,
error,
name,
setName,
description,
setDescription,
inputs,
setInputValue,
credentials,
setCredentialValue,
handleSaveChanges,
handleStartTask,
isSaving,
isStarting,
} = useSelectedTemplateView({
templateId,
graphId: agent.graph_id,
onRunCreated,
});
const agentInputFields = getAgentInputFields(agent);
const agentCredentialsFields = getAgentCredentialsFields(agent);
const inputFields = Object.entries(agentInputFields);
const credentialFields = Object.entries(agentCredentialsFields);
if (error) {
return (
<ErrorCard
responseError={
error
? {
message: String(
(error as unknown as { message?: string })?.message ||
"Failed to load template",
),
}
: undefined
}
httpError={
(error as any)?.status
? {
status: (error as any).status,
statusText: (error as any).statusText,
}
: undefined
}
context="template"
/>
);
}
if (isLoading && !template) {
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
}
if (!template) {
return null;
}
const templateOrTrigger = agent.trigger_setup_info ? "Trigger" : "Template";
const hasWebhook = !!template.webhook_id && template.webhook;
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={undefined} />
{hasWebhook && agent.trigger_setup_info && (
<WebhookTriggerCard
template={template}
triggerSetupInfo={agent.trigger_setup_info}
/>
)}
<RunDetailCard title={`${templateOrTrigger} Details`}>
<div className="flex flex-col gap-2">
<Input
id="template-name"
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={`Enter ${templateOrTrigger.toLowerCase()} name`}
/>
<Input
id="template-description"
label="Description"
type="textarea"
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={`Enter ${templateOrTrigger.toLowerCase()} description`}
/>
</div>
</RunDetailCard>
{inputFields.length > 0 && (
<RunDetailCard title="Your Input">
<div className="flex flex-col gap-4">
{inputFields.map(([key, inputSubSchema]) => (
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={inputs[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
/>
))}
</div>
</RunDetailCard>
)}
{credentialFields.length > 0 && (
<RunDetailCard title="Task Credentials">
<div className="flex flex-col gap-6">
{credentialFields.map(([key, inputSubSchema]) => (
<CredentialsInput
key={key}
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
selectedCredentials={
credentials[key] ?? inputSubSchema.default
}
onSelectCredentials={(value) =>
setCredentialValue(key, value!)
}
siblingInputs={inputs}
/>
))}
</div>
</RunDetailCard>
)}
</div>
</SelectedViewLayout>
</div>
{template ? (
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
<SelectedTemplateActions
agent={agent}
templateId={template.id}
onDeleted={onClearSelectedRun}
onSaveChanges={handleSaveChanges}
onStartTask={handleStartTask}
isSaving={isSaving}
isStarting={isStarting}
onSwitchToRunsTab={onSwitchToRunsTab}
/>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,174 @@
"use client";
import {
getGetV2ListPresetsQueryKey,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
import { okData } from "@/app/api/helpers";
import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { FloppyDiskIcon, PlayIcon, TrashIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
interface Props {
agent: LibraryAgent;
templateId: string;
onDeleted?: () => void;
onSaveChanges?: () => void;
onStartTask?: () => void;
isSaving?: boolean;
isStarting?: boolean;
onSwitchToRunsTab?: () => void;
}
export function SelectedTemplateActions({
agent,
templateId,
onDeleted,
onSaveChanges,
onStartTask,
isSaving,
isStarting,
onSwitchToRunsTab,
}: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const deleteMutation = useDeleteV2DeleteAPreset({
mutation: {
onSuccess: async () => {
toast({
title: "Template deleted",
});
const queryKey = getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
});
queryClient.invalidateQueries({
queryKey,
});
const queryData = queryClient.getQueryData<{
data: LibraryAgentPresetResponse;
}>(queryKey);
const presets =
okData<LibraryAgentPresetResponse>(queryData)?.presets ?? [];
const templates = presets.filter(
(preset) => !preset.webhook_id || !preset.webhook,
);
setShowDeleteDialog(false);
onDeleted?.();
if (templates.length === 0 && onSwitchToRunsTab) {
onSwitchToRunsTab();
}
},
onError: (error: any) => {
toast({
title: "Failed to delete template",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
function handleDelete() {
deleteMutation.mutate({ presetId: templateId });
}
return (
<>
<div className="my-4 flex flex-col items-center gap-3">
<Button
variant="icon"
size="icon"
aria-label="Save changes"
onClick={onSaveChanges}
disabled={isSaving || isStarting || deleteMutation.isPending}
>
{isSaving ? (
<LoadingSpinner size="small" />
) : (
<FloppyDiskIcon weight="bold" size={18} className="text-zinc-700" />
)}
</Button>
{onStartTask && (
<Button
variant="icon"
size="icon"
aria-label="Start task from template"
onClick={onStartTask}
disabled={isSaving || isStarting || deleteMutation.isPending}
>
{isStarting ? (
<>
<LoadingSpinner size="small" />
</>
) : (
<>
<PlayIcon weight="bold" size={16} />
</>
)}
</Button>
)}
<Button
variant="icon"
size="icon"
aria-label="Delete template"
onClick={() => setShowDeleteDialog(true)}
disabled={isSaving || isStarting || deleteMutation.isPending}
>
{deleteMutation.isPending ? (
<LoadingSpinner size="small" />
) : (
<TrashIcon weight="bold" size={18} />
)}
</Button>
<AgentActionsDropdown agent={agent} />
</div>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete template"
>
<Dialog.Content>
<Text variant="large">
Are you sure you want to delete this template? This action cannot be
undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
onClick={() => setShowDeleteDialog(false)}
disabled={deleteMutation.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={deleteMutation.isPending}
>
Delete
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,92 @@
"use client";
import { GraphTriggerInfo } from "@/app/api/__generated__/models/graphTriggerInfo";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { CopyIcon } from "@phosphor-icons/react";
import { RunDetailCard } from "../../RunDetailCard/RunDetailCard";
interface Props {
template: LibraryAgentPreset;
triggerSetupInfo: GraphTriggerInfo;
}
function getTriggerStatus(
template: LibraryAgentPreset,
): "active" | "inactive" | "broken" {
if (!template.webhook_id || !template.webhook) return "broken";
return template.is_active ? "active" : "inactive";
}
export function WebhookTriggerCard({ template, triggerSetupInfo }: Props) {
const status = getTriggerStatus(template);
const webhook = template.webhook;
function handleCopyWebhookUrl() {
if (webhook?.url) {
navigator.clipboard.writeText(webhook.url);
}
}
return (
<RunDetailCard title="Trigger Status">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Text variant="large-medium">Status</Text>
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
status === "active"
? "bg-green-100 text-green-800"
: status === "inactive"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{status === "active"
? "Active"
: status === "inactive"
? "Inactive"
: "Broken"}
</span>
</div>
{!template.webhook_id ? (
<Text variant="body" className="text-red-600">
This trigger is not attached to a webhook. Use &quot;Set up
trigger&quot; to fix this.
</Text>
) : !triggerSetupInfo.credentials_input_name && webhook ? (
<div className="flex flex-col gap-2">
<Text variant="body">
This trigger is ready to be used. Use the Webhook URL below to set
up the trigger connection with the service of your choosing.
</Text>
<div className="flex flex-col gap-1">
<Text variant="body-medium">Webhook URL:</Text>
<div className="flex gap-2 rounded-md bg-gray-50 p-2">
<code className="flex-1 select-all text-sm">{webhook.url}</code>
<Button
variant="outline"
size="icon"
className="size-7 flex-none p-1"
onClick={handleCopyWebhookUrl}
title="Copy webhook URL"
>
<CopyIcon className="size-4" />
</Button>
</div>
</div>
</div>
) : (
<Text variant="body" className="text-muted-foreground">
This agent trigger is{" "}
{template.is_active
? "ready. When a trigger is received, it will run with the provided settings."
: "disabled. It will not respond to triggers until you enable it."}
</Text>
)}
</div>
</RunDetailCard>
);
}

View File

@@ -0,0 +1,199 @@
"use client";
import { getGetV1ListGraphExecutionsInfiniteQueryOptions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2GetASpecificPresetQueryKey,
getGetV2ListPresetsQueryKey,
useGetV2GetASpecificPreset,
usePatchV2UpdateAnExistingPreset,
usePostV2ExecuteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import type { LibraryAgentPresetUpdatable } from "@/app/api/__generated__/models/libraryAgentPresetUpdatable";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
type Args = {
templateId: string;
graphId: string;
onRunCreated?: (execution: GraphExecutionMeta) => void;
};
export function useSelectedTemplateView({
templateId,
graphId,
onRunCreated,
}: Args) {
const { toast } = useToast();
const queryClient = useQueryClient();
const query = useGetV2GetASpecificPreset(templateId, {
query: {
enabled: !!templateId,
select: (res) => okData<LibraryAgentPreset>(res),
},
});
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [inputs, setInputs] = useState<Record<string, any>>({});
const [credentials, setCredentials] = useState<
Record<string, CredentialsMetaInput>
>({});
useEffect(() => {
if (query.data) {
setName(query.data.name || "");
setDescription(query.data.description || "");
setInputs(query.data.inputs || {});
setCredentials(query.data.credentials || {});
}
}, [query.data]);
const updateMutation = usePatchV2UpdateAnExistingPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Template updated",
});
queryClient.invalidateQueries({
queryKey: getGetV2GetASpecificPresetQueryKey(templateId),
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({ graph_id: graphId }),
});
}
},
onError: (error: any) => {
toast({
title: "Failed to update template",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const executeMutation = usePostV2ExecuteAPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
const execution = okData<GraphExecutionMeta>(response);
if (execution) {
toast({
title: "Task started",
});
queryClient.invalidateQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(graphId)
.queryKey,
});
onRunCreated?.(execution);
}
}
},
onError: (error: any) => {
toast({
title: "Failed to start task",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
function handleSaveChanges() {
if (!query.data) return;
const updateData: LibraryAgentPresetUpdatable = {};
if (name !== (query.data.name || "")) {
updateData.name = name;
}
if (description !== (query.data.description || "")) {
updateData.description = description;
}
const inputsChanged =
JSON.stringify(inputs) !== JSON.stringify(query.data.inputs || {});
const credentialsChanged =
JSON.stringify(credentials) !==
JSON.stringify(query.data.credentials || {});
if (inputsChanged || credentialsChanged) {
updateData.inputs = inputs;
updateData.credentials = credentials;
}
updateMutation.mutate({
presetId: templateId,
data: updateData,
});
}
function handleStartTask() {
if (!query.data) return;
const inputsChanged =
JSON.stringify(inputs) !== JSON.stringify(query.data.inputs || {});
const credentialsChanged =
JSON.stringify(credentials) !==
JSON.stringify(query.data.credentials || {});
// Use changed unpersisted inputs if applicable
executeMutation.mutate({
presetId: templateId,
data: {
inputs: inputsChanged ? inputs : undefined,
credential_inputs: credentialsChanged ? credentials : undefined,
},
});
}
function setInputValue(key: string, value: any) {
setInputs((prev) => ({ ...prev, [key]: value }));
}
function setCredentialValue(key: string, value: CredentialsMetaInput) {
setCredentials((prev) => ({ ...prev, [key]: value }));
}
const httpError =
query.isSuccess && !query.data
? { status: 404, statusText: "Not found" }
: undefined;
useEffect(() => {
if (updateMutation.isSuccess && query.data) {
setName(query.data.name || "");
setDescription(query.data.description || "");
setInputs(query.data.inputs || {});
setCredentials(query.data.credentials || {});
}
}, [updateMutation.isSuccess, query.data]);
return {
template: query.data,
isLoading: query.isLoading,
error: query.error || httpError,
name,
setName,
description,
setDescription,
inputs,
setInputValue,
credentials,
setCredentialValue,
handleSaveChanges,
handleStartTask,
isSaving: updateMutation.isPending,
isStarting: executeMutation.isPending,
} as const;
}

View File

@@ -0,0 +1,183 @@
"use client";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Input } from "@/components/atoms/Input/Input";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import {
getAgentCredentialsFields,
getAgentInputFields,
} from "../../modals/AgentInputsReadOnly/helpers";
import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInputs";
import { RunAgentInputs } from "../../modals/RunAgentInputs/RunAgentInputs";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
import { WebhookTriggerCard } from "../SelectedTemplateView/components/WebhookTriggerCard";
import { SelectedViewLayout } from "../SelectedViewLayout";
import { SelectedTriggerActions } from "./components/SelectedTriggerActions";
import { useSelectedTriggerView } from "./useSelectedTriggerView";
interface Props {
agent: LibraryAgent;
triggerId: string;
onClearSelectedRun?: () => void;
onSwitchToRunsTab?: () => void;
}
export function SelectedTriggerView({
agent,
triggerId,
onClearSelectedRun,
onSwitchToRunsTab,
}: Props) {
const {
trigger,
isLoading,
error,
name,
setName,
description,
setDescription,
inputs,
setInputValue,
credentials,
setCredentialValue,
handleSaveChanges,
isSaving,
} = useSelectedTriggerView({
triggerId,
graphId: agent.graph_id,
});
const agentInputFields = getAgentInputFields(agent);
const agentCredentialsFields = getAgentCredentialsFields(agent);
const inputFields = Object.entries(agentInputFields);
const credentialFields = Object.entries(agentCredentialsFields);
if (error) {
return (
<ErrorCard
responseError={
error
? {
message: String(
(error as unknown as { message?: string })?.message ||
"Failed to load trigger",
),
}
: undefined
}
httpError={
(error as any)?.status
? {
status: (error as any).status,
statusText: (error as any).statusText,
}
: undefined
}
context="trigger"
/>
);
}
if (isLoading && !trigger) {
return <LoadingSelectedContent agentName={agent.name} agentId={agent.id} />;
}
if (!trigger) {
return null;
}
const hasWebhook = !!trigger.webhook_id && trigger.webhook;
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={undefined} />
<RunDetailCard title="Trigger Details">
<div className="flex flex-col gap-2">
<Input
id="trigger-name"
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter trigger name"
/>
<Input
id="trigger-description"
label="Description"
type="textarea"
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter trigger description"
/>
</div>
</RunDetailCard>
{hasWebhook && agent.trigger_setup_info && (
<WebhookTriggerCard
template={trigger}
triggerSetupInfo={agent.trigger_setup_info}
/>
)}
{inputFields.length > 0 && (
<RunDetailCard title="Your Input">
<div className="flex flex-col gap-4">
{inputFields.map(([key, inputSubSchema]) => (
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={inputs[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
/>
))}
</div>
</RunDetailCard>
)}
{credentialFields.length > 0 && (
<RunDetailCard title="Task Credentials">
<div className="flex flex-col gap-6">
{credentialFields.map(([key, inputSubSchema]) => (
<CredentialsInput
key={key}
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
selectedCredentials={
credentials[key] ?? inputSubSchema.default
}
onSelectCredentials={(value) =>
setCredentialValue(key, value!)
}
siblingInputs={inputs}
/>
))}
</div>
</RunDetailCard>
)}
</div>
</SelectedViewLayout>
</div>
{trigger ? (
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
<SelectedTriggerActions
agent={agent}
triggerId={trigger.id}
onDeleted={onClearSelectedRun}
onSaveChanges={handleSaveChanges}
isSaving={isSaving}
onSwitchToRunsTab={onSwitchToRunsTab}
/>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,151 @@
"use client";
import {
getGetV2ListPresetsQueryKey,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
import { okData } from "@/app/api/helpers";
import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { FloppyDiskIcon, TrashIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
interface Props {
agent: LibraryAgent;
triggerId: string;
onDeleted?: () => void;
onSaveChanges?: () => void;
isSaving?: boolean;
onSwitchToRunsTab?: () => void;
}
export function SelectedTriggerActions({
agent,
triggerId,
onDeleted,
onSaveChanges,
isSaving,
onSwitchToRunsTab,
}: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const deleteMutation = useDeleteV2DeleteAPreset({
mutation: {
onSuccess: async () => {
toast({
title: "Trigger deleted",
});
const queryKey = getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
});
queryClient.invalidateQueries({
queryKey,
});
const queryData = queryClient.getQueryData<{
data: LibraryAgentPresetResponse;
}>(queryKey);
const presets =
okData<LibraryAgentPresetResponse>(queryData)?.presets ?? [];
const triggers = presets.filter(
(preset) => preset.webhook_id && preset.webhook,
);
setShowDeleteDialog(false);
onDeleted?.();
if (triggers.length === 0 && onSwitchToRunsTab) {
onSwitchToRunsTab();
}
},
onError: (error: any) => {
toast({
title: "Failed to delete trigger",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
function handleDelete() {
deleteMutation.mutate({ presetId: triggerId });
}
return (
<>
<div className="my-4 flex flex-col items-center gap-3">
<Button
variant="icon"
size="icon"
aria-label="Save changes"
onClick={onSaveChanges}
disabled={isSaving || deleteMutation.isPending}
>
{isSaving ? (
<LoadingSpinner size="small" />
) : (
<FloppyDiskIcon weight="bold" size={18} className="text-zinc-700" />
)}
</Button>
<Button
variant="icon"
size="icon"
aria-label="Delete trigger"
onClick={() => setShowDeleteDialog(true)}
disabled={isSaving || deleteMutation.isPending}
>
{deleteMutation.isPending ? (
<LoadingSpinner size="small" />
) : (
<TrashIcon weight="bold" size={18} />
)}
</Button>
<AgentActionsDropdown agent={agent} />
</div>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete trigger"
>
<Dialog.Content>
<Text variant="large">
Are you sure you want to delete this trigger? This action cannot be
undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
onClick={() => setShowDeleteDialog(false)}
disabled={deleteMutation.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={deleteMutation.isPending}
>
Delete
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,141 @@
"use client";
import {
getGetV2GetASpecificPresetQueryKey,
getGetV2ListPresetsQueryKey,
useGetV2GetASpecificPreset,
usePatchV2UpdateAnExistingPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import type { LibraryAgentPresetUpdatable } from "@/app/api/__generated__/models/libraryAgentPresetUpdatable";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
type Args = {
triggerId: string;
graphId: string;
};
export function useSelectedTriggerView({ triggerId, graphId }: Args) {
const { toast } = useToast();
const queryClient = useQueryClient();
const query = useGetV2GetASpecificPreset(triggerId, {
query: {
enabled: !!triggerId,
select: (res) => okData<LibraryAgentPreset>(res),
},
});
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [inputs, setInputs] = useState<Record<string, any>>({});
const [credentials, setCredentials] = useState<
Record<string, CredentialsMetaInput>
>({});
useEffect(() => {
if (query.data) {
setName(query.data.name || "");
setDescription(query.data.description || "");
setInputs(query.data.inputs || {});
setCredentials(query.data.credentials || {});
}
}, [query.data]);
const updateMutation = usePatchV2UpdateAnExistingPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Trigger updated",
});
queryClient.invalidateQueries({
queryKey: getGetV2GetASpecificPresetQueryKey(triggerId),
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({ graph_id: graphId }),
});
}
},
onError: (error: any) => {
toast({
title: "Failed to update trigger",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
function handleSaveChanges() {
if (!query.data) return;
const updateData: LibraryAgentPresetUpdatable = {};
if (name !== (query.data.name || "")) {
updateData.name = name;
}
if (description !== (query.data.description || "")) {
updateData.description = description;
}
const inputsChanged =
JSON.stringify(inputs) !== JSON.stringify(query.data.inputs || {});
const credentialsChanged =
JSON.stringify(credentials) !==
JSON.stringify(query.data.credentials || {});
if (inputsChanged || credentialsChanged) {
updateData.inputs = inputs;
updateData.credentials = credentials;
}
updateMutation.mutate({
presetId: triggerId,
data: updateData,
});
}
function setInputValue(key: string, value: any) {
setInputs((prev) => ({ ...prev, [key]: value }));
}
function setCredentialValue(key: string, value: CredentialsMetaInput) {
setCredentials((prev) => ({ ...prev, [key]: value }));
}
const httpError =
query.isSuccess && !query.data
? { status: 404, statusText: "Not found" }
: undefined;
useEffect(() => {
if (updateMutation.isSuccess && query.data) {
setName(query.data.name || "");
setDescription(query.data.description || "");
setInputs(query.data.inputs || {});
setCredentials(query.data.credentials || {});
}
}, [updateMutation.isSuccess, query.data]);
return {
trigger: query.data,
isLoading: query.isLoading,
error: query.error || httpError,
name,
setName,
description,
setDescription,
inputs,
setInputValue,
credentials,
setCredentialValue,
handleSaveChanges,
isSaving: updateMutation.isPending,
} as const;
}

View File

@@ -12,7 +12,7 @@ export function SelectedViewLayout(props: Props) {
return (
<SectionWrap className="relative mb-3 flex min-h-0 flex-1 flex-col">
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-4`}
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
>
<Breadcrumbs
items={[

View File

@@ -14,19 +14,26 @@ import {
} from "@/components/molecules/TabsLine/TabsLine";
import { cn } from "@/lib/utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { RunListItem } from "./components/RunListItem";
import { ScheduleListItem } from "./components/ScheduleListItem";
import { TaskListItem } from "./components/TaskListItem";
import { TemplateListItem } from "./components/TemplateListItem";
import { TriggerListItem } from "./components/TriggerListItem";
import { useSidebarRunsList } from "./useSidebarRunsList";
interface Props {
agent: LibraryAgent;
selectedRunId?: string;
onSelectRun: (id: string, tab?: "runs" | "scheduled") => void;
onSelectRun: (
id: string,
tab?: "runs" | "scheduled" | "templates" | "triggers",
) => void;
onClearSelectedRun?: () => void;
onTabChange?: (tab: "runs" | "scheduled" | "templates") => void;
onTabChange?: (tab: "runs" | "scheduled" | "templates" | "triggers") => void;
onCountsChange?: (info: {
runsCount: number;
schedulesCount: number;
templatesCount: number;
triggersCount: number;
loading?: boolean;
}) => void;
}
@@ -42,8 +49,12 @@ export function SidebarRunsList({
const {
runs,
schedules,
templates,
triggers,
runsCount,
schedulesCount,
templatesCount,
triggersCount,
error,
loading,
fetchMoreRuns,
@@ -79,7 +90,7 @@ export function SidebarRunsList({
<TabsLine
value={tabValue}
onValueChange={(v) => {
const value = v as "runs" | "scheduled" | "templates";
const value = v as "runs" | "scheduled" | "templates" | "triggers";
onTabChange?.(value);
if (value === "runs") {
if (runs && runs.length) {
@@ -95,21 +106,38 @@ export function SidebarRunsList({
}
} else if (value === "templates") {
onClearSelectedRun?.();
} else if (value === "triggers") {
onClearSelectedRun?.();
}
}}
className="flex min-h-0 flex-col overflow-hidden"
>
<TabsLineList className={AGENT_LIBRARY_SECTION_PADDING_X}>
<TabsLineTrigger value="runs">
Tasks <span className="ml-3 inline-block">{runsCount}</span>
</TabsLineTrigger>
<TabsLineTrigger value="scheduled">
Scheduled <span className="ml-3 inline-block">{schedulesCount}</span>
</TabsLineTrigger>
<TabsLineTrigger value="templates">
Templates <span className="ml-3 inline-block">0</span>
</TabsLineTrigger>
</TabsLineList>
<div className="relative overflow-hidden">
<div className="pointer-events-none absolute right-0 top-0 z-10 h-[46px] w-12 bg-gradient-to-l from-[#FAFAFA] to-transparent" />
<div className="scrollbar-hide overflow-x-auto">
<TabsLineList
className={cn(AGENT_LIBRARY_SECTION_PADDING_X, "min-w-max")}
>
<TabsLineTrigger value="runs">
Tasks <span className="ml-3 inline-block">{runsCount}</span>
</TabsLineTrigger>
<TabsLineTrigger value="scheduled">
Scheduled{" "}
<span className="ml-3 inline-block">{schedulesCount}</span>
</TabsLineTrigger>
{triggersCount > 0 && (
<TabsLineTrigger value="triggers">
Triggers{" "}
<span className="ml-3 inline-block">{triggersCount}</span>
</TabsLineTrigger>
)}
<TabsLineTrigger value="templates">
Templates{" "}
<span className="ml-3 inline-block">{templatesCount}</span>
</TabsLineTrigger>
</TabsLineList>
</div>
</div>
<>
<TabsLineContent
@@ -128,9 +156,10 @@ export function SidebarRunsList({
itemWrapperClassName="w-auto lg:w-full"
renderItem={(run) => (
<div className="w-[15rem] lg:w-full">
<RunListItem
<TaskListItem
run={run}
title={agent.name}
agent={agent}
selected={selectedRunId === run.id}
onClick={() => onSelectRun && onSelectRun(run.id, "runs")}
/>
@@ -151,6 +180,7 @@ export function SidebarRunsList({
<div className="w-[15rem] lg:w-full" key={s.id}>
<ScheduleListItem
schedule={s}
agent={agent}
selected={selectedRunId === s.id}
onClick={() => onSelectRun(s.id, "scheduled")}
/>
@@ -165,6 +195,36 @@ export function SidebarRunsList({
)}
</div>
</TabsLineContent>
{triggersCount > 0 && (
<TabsLineContent
value="triggers"
className={cn(
"mt-0 flex min-h-0 flex-1 flex-col",
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden">
{triggers.length > 0 ? (
triggers.map((trigger) => (
<div className="w-[15rem] lg:w-full" key={trigger.id}>
<TriggerListItem
trigger={trigger}
agent={agent}
selected={selectedRunId === trigger.id}
onClick={() => onSelectRun(trigger.id, "triggers")}
/>
</div>
))
) : (
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<Text variant="large" className="text-zinc-700">
No triggers set up
</Text>
</div>
)}
</div>
</TabsLineContent>
)}
<TabsLineContent
value="templates"
className={cn(
@@ -173,11 +233,24 @@ export function SidebarRunsList({
)}
>
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden">
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<Text variant="large" className="text-zinc-700">
No templates saved
</Text>
</div>
{templates.length > 0 ? (
templates.map((template) => (
<div className="w-[15rem] lg:w-full" key={template.id}>
<TemplateListItem
template={template}
agent={agent}
selected={selectedRunId === template.id}
onClick={() => onSelectRun(template.id, "templates")}
/>
</div>
))
) : (
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<Text variant="large" className="text-zinc-700">
No templates saved
</Text>
</div>
)}
</div>
</TabsLineContent>
</>

View File

@@ -0,0 +1,123 @@
"use client";
import {
getGetV1ListExecutionSchedulesForAGraphQueryOptions,
useDeleteV1DeleteExecutionSchedule,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
schedule: GraphExecutionJobInfo;
onDeleted?: () => void;
}
export function ScheduleActionsDropdown({ agent, schedule, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutateAsync: deleteSchedule, isPending: isDeleting } =
useDeleteV1DeleteExecutionSchedule();
async function handleDelete() {
try {
await deleteSchedule({ scheduleId: schedule.id });
toast({ title: "Schedule deleted" });
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryOptions(
agent.graph_id,
).queryKey,
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete schedule",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete schedule
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete schedule"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this schedule? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete Schedule
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -1,38 +1,50 @@
"use client";
import React from "react";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import moment from "moment";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./RunIconWrapper";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
import moment from "moment";
import { IconWrapper } from "./IconWrapper";
import { ScheduleActionsDropdown } from "./ScheduleActionsDropdown";
import { SidebarItemCard } from "./SidebarItemCard";
interface ScheduleListItemProps {
interface Props {
schedule: GraphExecutionJobInfo;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function ScheduleListItem({
schedule,
agent,
selected,
onClick,
}: ScheduleListItemProps) {
onDeleted,
}: Props) {
return (
<RunSidebarCard
<SidebarItemCard
title={schedule.name}
description={moment(schedule.next_run_time).fromNow()}
onClick={onClick}
selected={selected}
icon={
<IconWrapper className="border-slate-50 bg-slate-50">
<IconWrapper className="border-slate-50 bg-yellow-50">
<ClockClockwiseIcon
size={16}
className="text-slate-700"
className="text-yellow-700"
weight="bold"
/>
</IconWrapper>
}
actions={
<ScheduleActionsDropdown
agent={agent}
schedule={schedule}
onDeleted={onDeleted}
/>
}
/>
);
}

View File

@@ -4,25 +4,27 @@ import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import React from "react";
interface RunListItemProps {
interface Props {
title: string;
description?: string;
icon?: React.ReactNode;
selected?: boolean;
onClick?: () => void;
actions?: React.ReactNode;
}
export function RunSidebarCard({
export function SidebarItemCard({
title,
description,
icon,
selected,
onClick,
}: RunListItemProps) {
actions,
}: Props) {
return (
<button
<div
className={cn(
"w-full rounded-large border border-zinc-200 bg-white p-3 text-left ring-1 ring-transparent transition-all duration-150 hover:scale-[1.01] hover:bg-slate-50/50",
"w-full cursor-pointer rounded-large border border-zinc-200 bg-white p-3 text-left ring-1 ring-transparent transition-all duration-150 hover:scale-[1.01] hover:bg-slate-50/50",
selected ? "border-slate-800 ring-slate-800" : undefined,
)}
onClick={onClick}
@@ -40,7 +42,10 @@ export function RunSidebarCard({
{description}
</Text>
</div>
{actions ? (
<div onClick={(e) => e.stopPropagation()}>{actions}</div>
) : null}
</div>
</button>
</div>
);
}

View File

@@ -0,0 +1,185 @@
"use client";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
useDeleteV1DeleteGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2ListPresetsQueryKey,
usePostV2CreateANewPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { CreateTemplateModal } from "../../../selected-views/SelectedRunView/components/CreateTemplateModal/CreateTemplateModal";
interface Props {
agent: LibraryAgent;
run: GraphExecutionMeta;
onDeleted?: () => void;
}
export function TaskActionsDropdown({ agent, run, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isCreateTemplateModalOpen, setIsCreateTemplateModalOpen] =
useState(false);
const { mutateAsync: deleteRun, isPending: isDeletingRun } =
useDeleteV1DeleteGraphExecution();
const { mutateAsync: createPreset } = usePostV2CreateANewPreset();
async function handleDeleteRun() {
try {
await deleteRun({ graphExecId: run.id });
toast({ title: "Task deleted" });
await queryClient.refetchQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
agent.graph_id,
).queryKey,
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete task",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
async function handleCreateTemplate(name: string, description: string) {
try {
const res = await createPreset({
data: {
name,
description,
graph_execution_id: run.id,
},
});
if (res.status === 200) {
toast({
title: "Template created",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
setIsCreateTemplateModalOpen(false);
}
} catch (error: unknown) {
toast({
title: "Failed to create template",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setIsCreateTemplateModalOpen(true);
}}
className="flex items-center gap-2"
>
Save as template
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete task
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete task"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this task? This action cannot be
undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeletingRun}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteRun}
loading={isDeletingRun}
>
Delete Task
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
<CreateTemplateModal
isOpen={isCreateTemplateModalOpen}
onClose={() => setIsCreateTemplateModalOpen(false)}
onCreate={handleCreateTemplate}
run={run as any}
/>
</>
);
}

View File

@@ -2,6 +2,7 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import {
CheckCircleIcon,
ClockIcon,
@@ -12,8 +13,9 @@ import {
} from "@phosphor-icons/react";
import moment from "moment";
import React from "react";
import { IconWrapper } from "./RunIconWrapper";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TaskActionsDropdown } from "./TaskActionsDropdown";
const statusIconMap: Record<AgentExecutionStatus, React.ReactNode> = {
INCOMPLETE: (
@@ -53,26 +55,33 @@ const statusIconMap: Record<AgentExecutionStatus, React.ReactNode> = {
),
};
interface RunListItemProps {
interface Props {
run: GraphExecutionMeta;
title: string;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function RunListItem({
export function TaskListItem({
run,
title,
agent,
selected,
onClick,
}: RunListItemProps) {
onDeleted,
}: Props) {
return (
<RunSidebarCard
<SidebarItemCard
icon={statusIconMap[run.status]}
title={title}
description={moment(run.started_at).fromNow()}
onClick={onClick}
selected={selected}
actions={
<TaskActionsDropdown agent={agent} run={run} onDeleted={onDeleted} />
}
/>
);
}

View File

@@ -0,0 +1,125 @@
"use client";
import {
getGetV2ListPresetsQueryKey,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
template: LibraryAgentPreset;
onDeleted?: () => void;
}
export function TemplateActionsDropdown({ agent, template, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutateAsync: deletePreset, isPending: isDeleting } =
useDeleteV2DeleteAPreset();
async function handleDelete() {
try {
await deletePreset({ presetId: template.id });
toast({
title: "Template deleted",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete template",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete template
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete template"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this template? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete Template
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { FileTextIcon } from "@phosphor-icons/react";
import moment from "moment";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TemplateActionsDropdown } from "./TemplateActionsDropdown";
interface Props {
template: LibraryAgentPreset;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function TemplateListItem({
template,
agent,
selected,
onClick,
onDeleted,
}: Props) {
return (
<SidebarItemCard
icon={
<IconWrapper className="border-blue-50 bg-blue-50">
<FileTextIcon size={16} className="text-zinc-700" weight="bold" />
</IconWrapper>
}
title={template.name}
description={moment(template.updated_at).fromNow()}
onClick={onClick}
selected={selected}
actions={
<TemplateActionsDropdown
agent={agent}
template={template}
onDeleted={onDeleted}
/>
}
/>
);
}

View File

@@ -0,0 +1,125 @@
"use client";
import {
getGetV2ListPresetsQueryKey,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
trigger: LibraryAgentPreset;
onDeleted?: () => void;
}
export function TriggerActionsDropdown({ agent, trigger, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutateAsync: deletePreset, isPending: isDeleting } =
useDeleteV2DeleteAPreset();
async function handleDelete() {
try {
await deletePreset({ presetId: trigger.id });
toast({
title: "Trigger deleted",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete trigger",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete trigger
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete trigger"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this trigger? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete Trigger
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { LightningIcon } from "@phosphor-icons/react";
import moment from "moment";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TriggerActionsDropdown } from "./TriggerActionsDropdown";
interface Props {
trigger: LibraryAgentPreset;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function TriggerListItem({
trigger,
agent,
selected,
onClick,
onDeleted,
}: Props) {
return (
<SidebarItemCard
icon={
<IconWrapper className="border-purple-50 bg-purple-50">
<LightningIcon size={16} className="text-zinc-700" weight="bold" />
</IconWrapper>
}
title={trigger.name}
description={moment(trigger.updated_at).fromNow()}
onClick={onClick}
selected={selected}
actions={
<TriggerActionsDropdown
agent={agent}
trigger={trigger}
onDeleted={onDeleted}
/>
}
/>
);
}

View File

@@ -3,8 +3,10 @@
import { useEffect, useMemo } from "react";
import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2ListPresets } from "@/app/api/__generated__/endpoints/presets/presets";
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
import { okData } from "@/app/api/helpers";
import { useExecutionEvents } from "@/hooks/useExecutionEvents";
import { useQueryClient } from "@tanstack/react-query";
@@ -15,19 +17,31 @@ import {
getNextRunsPageParam,
} from "./helpers";
function parseTab(value: string | null): "runs" | "scheduled" | "templates" {
if (value === "runs" || value === "scheduled" || value === "templates") {
function parseTab(
value: string | null,
): "runs" | "scheduled" | "templates" | "triggers" {
if (
value === "runs" ||
value === "scheduled" ||
value === "templates" ||
value === "triggers"
) {
return value;
}
return "runs";
}
type Args = {
graphId?: string;
onSelectRun: (runId: string, tab?: "runs" | "scheduled") => void;
graphId: string;
onSelectRun: (
runId: string,
tab?: "runs" | "scheduled" | "templates" | "triggers",
) => void;
onCountsChange?: (info: {
runsCount: number;
schedulesCount: number;
templatesCount: number;
triggersCount: number;
loading?: boolean;
}) => void;
};
@@ -46,7 +60,7 @@ export function useSidebarRunsList({
const queryClient = useQueryClient();
const runsQuery = useGetV1ListGraphExecutionsInfinite(
graphId || "",
graphId,
{ page: 1, page_size: 20 },
{
query: {
@@ -57,12 +71,19 @@ export function useSidebarRunsList({
},
);
const schedulesQuery = useGetV1ListExecutionSchedulesForAGraph(
graphId || "",
const schedulesQuery = useGetV1ListExecutionSchedulesForAGraph(graphId, {
query: {
enabled: !!graphId,
select: (r) => okData<GraphExecutionJobInfo[]>(r),
},
});
const presetsQuery = useGetV2ListPresets(
{ graph_id: graphId, page: 1, page_size: 100 },
{
query: {
enabled: !!graphId,
select: (r) => okData<GraphExecutionJobInfo[]>(r) ?? [],
select: (r) => okData<LibraryAgentPresetResponse>(r)?.presets,
},
},
);
@@ -73,10 +94,26 @@ export function useSidebarRunsList({
);
const schedules = schedulesQuery.data || [];
const allPresets = presetsQuery.data || [];
const triggers = useMemo(
() => allPresets.filter((preset) => preset.webhook_id),
[allPresets],
);
const templates = useMemo(
() => allPresets.filter((preset) => !preset.webhook_id),
[allPresets],
);
const runsCount = computeRunsCount(runsQuery.data, runs.length);
const schedulesCount = schedules.length;
const loading = !schedulesQuery.isSuccess || !runsQuery.isSuccess;
const templatesCount = templates.length;
const triggersCount = triggers.length;
const loading =
!runsQuery.isSuccess ||
!schedulesQuery.isSuccess ||
!presetsQuery.isSuccess;
const stale =
runsQuery.isStale || schedulesQuery.isStale || presetsQuery.isStale;
// Update query cache when execution events arrive via websocket
useExecutionEvents({
@@ -93,10 +130,24 @@ export function useSidebarRunsList({
// Notify parent about counts and loading state
useEffect(() => {
if (onCountsChange) {
onCountsChange({ runsCount, schedulesCount, loading });
if (onCountsChange && !stale) {
onCountsChange({
runsCount,
schedulesCount,
templatesCount,
triggersCount,
loading,
});
}
}, [runsCount, schedulesCount, loading, onCountsChange]);
}, [
onCountsChange,
runsCount,
schedulesCount,
templatesCount,
triggersCount,
loading,
stale,
]);
useEffect(() => {
if (runs.length > 0 && tabValue === "runs" && !activeItem) {
@@ -111,15 +162,31 @@ export function useSidebarRunsList({
}
}, [activeItem, runs.length, schedules, onSelectRun]);
useEffect(() => {
if (templates.length > 0 && tabValue === "templates" && !activeItem) {
onSelectRun(templates[0].id, "templates");
}
}, [templates, activeItem, tabValue, onSelectRun]);
useEffect(() => {
if (triggers.length > 0 && tabValue === "triggers" && !activeItem) {
onSelectRun(triggers[0].id, "triggers");
}
}, [triggers, activeItem, tabValue, onSelectRun]);
return {
runs,
schedules,
error: schedulesQuery.error || runsQuery.error,
templates,
triggers,
error: schedulesQuery.error || runsQuery.error || presetsQuery.error,
loading,
runsQuery,
tabValue,
runsCount,
schedulesCount,
templatesCount,
triggersCount,
fetchMoreRuns: runsQuery.fetchNextPage,
hasMoreRuns: runsQuery.hasNextPage,
isFetchingMoreRuns: runsQuery.isFetchingNextPage,

View File

@@ -1,12 +1,23 @@
import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
import { useGetV2GetASpecificPreset } from "@/app/api/__generated__/endpoints/presets/presets";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { okData } from "@/app/api/helpers";
import { useParams } from "next/navigation";
import { parseAsString, useQueryStates } from "nuqs";
import { useCallback, useEffect, useMemo, useState } from "react";
function parseTab(value: string | null): "runs" | "scheduled" | "templates" {
if (value === "runs" || value === "scheduled" || value === "templates") {
function parseTab(
value: string | null,
): "runs" | "scheduled" | "templates" | "triggers" {
if (
value === "runs" ||
value === "scheduled" ||
value === "templates" ||
value === "triggers"
) {
return value;
}
return "runs";
@@ -17,7 +28,7 @@ export function useNewAgentLibraryView() {
const agentId = id as string;
const {
data: response,
data: agent,
isSuccess,
error,
} = useGetV2GetLibraryAgent(agentId, {
@@ -34,6 +45,24 @@ export function useNewAgentLibraryView() {
const activeTab = useMemo(() => parseTab(activeTabRaw), [activeTabRaw]);
const {
data: _template,
isSuccess: isTemplateLoaded,
isLoading: isTemplateLoading,
error: templateError,
} = useGetV2GetASpecificPreset(activeItem ?? "", {
query: {
enabled: Boolean(activeTab === "templates" && activeItem),
select: okData<LibraryAgentPreset>,
},
});
const activeTemplate =
isTemplateLoaded &&
activeTab === "templates" &&
_template?.id === activeItem
? _template
: null;
useEffect(() => {
if (!activeTabRaw && !activeItem) {
setQueryStates({
@@ -45,6 +74,8 @@ export function useNewAgentLibraryView() {
const [sidebarCounts, setSidebarCounts] = useState({
runsCount: 0,
schedulesCount: 0,
templatesCount: 0,
triggersCount: 0,
});
const [sidebarLoading, setSidebarLoading] = useState(true);
@@ -52,7 +83,9 @@ export function useNewAgentLibraryView() {
const hasAnyItems = useMemo(
() =>
(sidebarCounts.runsCount ?? 0) > 0 ||
(sidebarCounts.schedulesCount ?? 0) > 0,
(sidebarCounts.schedulesCount ?? 0) > 0 ||
(sidebarCounts.templatesCount ?? 0) > 0 ||
(sidebarCounts.triggersCount ?? 0) > 0,
[sidebarCounts],
);
@@ -60,12 +93,27 @@ export function useNewAgentLibraryView() {
const showSidebarLayout = sidebarLoading || hasAnyItems;
useEffect(() => {
if (response) {
document.title = `${response.name} - Library - AutoGPT Platform`;
if (agent) {
document.title = `${agent.name} - Library - AutoGPT Platform`;
}
}, [response]);
}, [agent]);
function handleSelectRun(id: string, tab?: "runs" | "scheduled") {
useEffect(() => {
if (
activeTab === "triggers" &&
sidebarCounts.triggersCount === 0 &&
!sidebarLoading
) {
setQueryStates({
activeTab: "runs",
});
}
}, [activeTab, sidebarCounts.triggersCount, sidebarLoading, setQueryStates]);
function handleSelectRun(
id: string,
tab?: "runs" | "scheduled" | "templates" | "triggers",
) {
setQueryStates({
activeItem: id,
activeTab: tab ?? "runs",
@@ -78,7 +126,9 @@ export function useNewAgentLibraryView() {
});
}
function handleSetActiveTab(tab: "runs" | "scheduled" | "templates") {
function handleSetActiveTab(
tab: "runs" | "scheduled" | "templates" | "triggers",
) {
setQueryStates({
activeTab: tab,
});
@@ -88,11 +138,15 @@ export function useNewAgentLibraryView() {
(counts: {
runsCount: number;
schedulesCount: number;
templatesCount: number;
triggersCount: number;
loading?: boolean;
}) => {
setSidebarCounts({
runsCount: counts.runsCount,
schedulesCount: counts.schedulesCount,
templatesCount: counts.templatesCount,
triggersCount: counts.triggersCount,
});
if (counts.loading !== undefined) {
setSidebarLoading(counts.loading);
@@ -101,11 +155,46 @@ export function useNewAgentLibraryView() {
[],
);
function onItemCreated(
createEvent:
| { type: "runs"; item: GraphExecutionMeta }
| { type: "triggers"; item: LibraryAgentPreset }
| { type: "scheduled"; item: GraphExecutionJobInfo },
) {
if (!hasAnyItems) {
// Manually increment item count to flip hasAnyItems and showSidebarLayout
const counts = {
runsCount: createEvent.type === "runs" ? 1 : 0,
triggersCount: createEvent.type === "triggers" ? 1 : 0,
schedulesCount: createEvent.type === "scheduled" ? 1 : 0,
templatesCount: 0,
};
handleCountsChange(counts);
}
}
function onRunInitiated(newRun: GraphExecutionMeta) {
if (!agent) return;
onItemCreated({ item: newRun, type: "runs" });
}
function onTriggerSetup(newTrigger: LibraryAgentPreset) {
if (!agent) return;
onItemCreated({ item: newTrigger, type: "triggers" });
}
function onScheduleCreated(newSchedule: GraphExecutionJobInfo) {
if (!agent) return;
onItemCreated({ item: newSchedule, type: "scheduled" });
}
return {
agentId: id,
agent,
ready: isSuccess,
error,
agent: response,
activeTemplate,
isTemplateLoading,
error: error || templateError,
hasAnyItems,
showSidebarLayout,
activeItem,
@@ -115,5 +204,8 @@ export function useNewAgentLibraryView() {
handleClearSelectedRun,
handleCountsChange,
handleSelectRun,
onRunInitiated,
onTriggerSetup,
onScheduleCreated,
};
}

View File

@@ -680,28 +680,20 @@ export function AgentRunDraftView({
{/* Regular inputs */}
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
<div key={key} className="flex flex-col space-y-2">
<label className="flex items-center gap-1 text-sm font-medium">
{inputSubSchema.title || key}
<InformationTooltip
description={inputSubSchema.description}
/>
</label>
<RunAgentInputs
schema={inputSubSchema}
value={inputValues[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => {
setInputValues((obj) => ({
...obj,
[key]: value,
}));
setChangedPresetAttributes((prev) => prev.add("inputs"));
}}
data-testid={`agent-input-${key}`}
/>
</div>
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={inputValues[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => {
setInputValues((obj) => ({
...obj,
[key]: value,
}));
setChangedPresetAttributes((prev) => prev.add("inputs"));
}}
data-testid={`agent-input-${key}`}
/>
))}
</CardContent>
</Card>

View File

@@ -3662,7 +3662,18 @@
"required": false,
"schema": {
"anyOf": [
{ "type": "array", "items": { "type": "string" } },
{
"type": "array",
"items": {
"enum": [
"blocks",
"integrations",
"marketplace_agents",
"my_agents"
],
"type": "string"
}
},
{ "type": "null" }
],
"title": "Filter"
@@ -8612,6 +8623,45 @@
"required": ["name", "cron", "inputs"],
"title": "ScheduleCreationRequest"
},
"SearchEntry": {
"properties": {
"search_query": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Search Query"
},
"filter": {
"anyOf": [
{
"items": {
"type": "string",
"enum": [
"blocks",
"integrations",
"marketplace_agents",
"my_agents"
]
},
"type": "array"
},
{ "type": "null" }
],
"title": "Filter"
},
"by_creator": {
"anyOf": [
{ "items": { "type": "string" }, "type": "array" },
{ "type": "null" }
],
"title": "By Creator"
},
"search_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Search Id"
}
},
"type": "object",
"title": "SearchEntry"
},
"SearchResponse": {
"properties": {
"items": {
@@ -8625,6 +8675,7 @@
"type": "array",
"title": "Items"
},
"search_id": { "type": "string", "title": "Search Id" },
"total_items": {
"additionalProperties": { "type": "integer" },
"propertyNames": {
@@ -8638,11 +8689,10 @@
"type": "object",
"title": "Total Items"
},
"page": { "type": "integer", "title": "Page" },
"more_pages": { "type": "boolean", "title": "More Pages" }
"pagination": { "$ref": "#/components/schemas/Pagination" }
},
"type": "object",
"required": ["items", "total_items", "page", "more_pages"],
"required": ["items", "search_id", "total_items", "pagination"],
"title": "SearchResponse"
},
"SessionDetailResponse": {
@@ -9199,7 +9249,7 @@
"title": "Otto Suggestions"
},
"recent_searches": {
"items": { "type": "string" },
"items": { "$ref": "#/components/schemas/SearchEntry" },
"type": "array",
"title": "Recent Searches"
},

View File

@@ -6,6 +6,10 @@ import {
import { environment } from "@/services/environment";
import { NextRequest, NextResponse } from "next/server";
// Increase body size limit to 256MB to match backend file upload limit
export const maxDuration = 300; // 5 minutes timeout for large uploads
export const dynamic = "force-dynamic";
function buildBackendUrl(path: string[], queryString: string): string {
const backendPath = path.join("/");
return `${environment.getAGPTServerBaseUrl()}/${backendPath}${queryString}`;

View File

@@ -1,36 +1,33 @@
"use client";
import { LaunchDarklyProvider } from "@/services/feature-flags/feature-flag-provider";
import OnboardingProvider from "@/providers/onboarding/onboarding-provider";
import { TooltipProvider } from "@/components/atoms/Tooltip/BaseTooltip";
import { SentryUserTracker } from "@/components/monitor/SentryUserTracker";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps,
} from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { TooltipProvider } from "@/components/atoms/Tooltip/BaseTooltip";
import CredentialsProvider from "@/providers/agent-credentials/credentials-provider";
import { SentryUserTracker } from "@/components/monitor/SentryUserTracker";
import OnboardingProvider from "@/providers/onboarding/onboarding-provider";
import { LaunchDarklyProvider } from "@/services/feature-flags/feature-flag-provider";
import { QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider, ThemeProviderProps } from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
export function Providers({ children, ...props }: ThemeProviderProps) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<NextThemesProvider {...props}>
<BackendAPIProvider>
<SentryUserTracker />
<CredentialsProvider>
<LaunchDarklyProvider>
<OnboardingProvider>
<BackendAPIProvider>
<SentryUserTracker />
<CredentialsProvider>
<LaunchDarklyProvider>
<OnboardingProvider>
<ThemeProvider forcedTheme="light" {...props}>
<TooltipProvider>{children}</TooltipProvider>
</OnboardingProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
</ThemeProvider>
</OnboardingProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NuqsAdapter>
</QueryClientProvider>
);

View File

@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-slate-50", className)}
className={cn("animate-pulse rounded-md bg-zinc-100", className)}
{...props}
/>
);

View File

@@ -16,7 +16,7 @@ export const extendedButtonVariants = cva(
primary:
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
secondary:
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
"bg-zinc-200 border-zinc-200 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
destructive:
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
outline:

View File

@@ -266,6 +266,7 @@ export function FileInput(props: Props) {
size="small"
className="h-7 w-7 min-w-0 flex-shrink-0 border-zinc-300 p-0 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500"
onClick={handleClear}
type="button"
>
<Cross2Icon className="h-3.5 w-3.5" />
</Button>
@@ -278,6 +279,7 @@ export function FileInput(props: Props) {
onClick={() => inputRef.current?.click()}
className="flex-1 border-zinc-300 text-xs"
disabled={isUploading}
type="button"
>
<UploadIcon className="mr-1.5 h-3.5 w-3.5" />
{`Upload ${displayName}`}
@@ -367,6 +369,7 @@ export function FileInput(props: Props) {
<Button
onClick={() => inputRef.current?.click()}
className="min-w-40"
type="button"
>
Browse File
</Button>

View File

@@ -1,4 +1,6 @@
import { Button } from "@/components/__legacy__/ui/button";
import { scrollbarStyles } from "@/components/styles/scrollbars";
import { cn } from "@/lib/utils";
import { X } from "@phosphor-icons/react";
import { PropsWithChildren } from "react";
import { Drawer } from "vaul";
@@ -41,7 +43,7 @@ export function DrawerWrap({
onInteractOutside={handleClose}
>
<div
className={`flex w-full items-center justify-between ${
className={`flex w-full shrink-0 items-center justify-between ${
title ? "pb-6" : "pb-0"
}`}
>
@@ -61,7 +63,16 @@ export function DrawerWrap({
)
) : null}
</div>
<div>{children}</div>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div
className={cn(
"flex-1 overflow-y-auto overflow-x-hidden",
scrollbarStyles,
)}
>
{children}
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
);

View File

@@ -19,5 +19,5 @@ export const modalStyles = {
// Drawer specific styles
export const drawerStyles = {
...commonStyles,
content: `${commonStyles.content} max-h-[90vh] w-full bottom-0 rounded-br-none rounded-bl-none`,
content: `${commonStyles.content} max-h-[90vh] w-full bottom-0 rounded-br-none rounded-bl-none min-h-0`,
};

View File

@@ -9,16 +9,20 @@ import ReactMarkdown from "react-markdown";
type Props = {
description?: string;
iconSize?: number;
};
export function InformationTooltip({ description }: Props) {
export function InformationTooltip({ description, iconSize = 24 }: Props) {
if (!description) return null;
return (
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="rounded-full p-1 hover:bg-slate-50" size={24} />
<Info
className="rounded-full p-1 hover:bg-slate-50"
size={iconSize}
/>
</TooltipTrigger>
<TooltipContent>
<ReactMarkdown

View File

@@ -0,0 +1,437 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import {
ScrollableTabs,
ScrollableTabsContent,
ScrollableTabsList,
ScrollableTabsTrigger,
} from "./ScrollableTabs";
const meta = {
title: "Molecules/ScrollableTabs",
component: ScrollableTabs,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof ScrollableTabs>;
export default meta;
type Story = StoryObj<typeof meta>;
function ScrollableTabsDemo() {
return (
<div className="flex flex-col gap-8 p-8">
<h2 className="text-2xl font-bold">ScrollableTabs Examples</h2>
<div className="space-y-6">
<div>
<h3 className="mb-4 text-lg font-semibold">
Short Content (Tabs Hidden)
</h3>
<div className="h-[300px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="tab1" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="tab1">
Account
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab2">
Password
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab3">
Settings
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="tab1">
<div className="p-4 text-sm">
Make changes to your account here. Click save when you&apos;re
done.
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab2">
<div className="p-4 text-sm">
Change your password here. After saving, you&apos;ll be logged
out.
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab3">
<div className="p-4 text-sm">
Update your preferences and settings here.
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Long Content (Tabs Visible)
</h3>
<div className="h-[400px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="tab1" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="tab1">
Account
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab2">
Password
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab3">
Settings
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="tab1">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
Account Settings
</h4>
<p className="mb-4">
Make changes to your account here. Click save when
you&apos;re done.
</p>
<p className="mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris.
</p>
<p className="mb-4">
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit
voluptatem accusantium doloremque laudantium, totam rem
aperiam.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab2">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
Password Settings
</h4>
<p className="mb-4">
Change your password here. After saving, you&apos;ll be
logged out.
</p>
<p className="mb-4">
At vero eos et accusamus et iusto odio dignissimos ducimus
qui blanditiis praesentium voluptatum deleniti atque
corrupti quos dolores et quas molestias excepturi sint
occaecati cupiditate.
</p>
<p className="mb-4">
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore, cum soluta nobis est eligendi optio
cumque nihil impedit quo minus.
</p>
<p>
Temporibus autem quibusdam et aut officiis debitis aut rerum
necessitatibus saepe eveniet ut et voluptates repudiandae
sint.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab3">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
General Settings
</h4>
<p className="mb-4">
Update your preferences and settings here.
</p>
<p className="mb-4">
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit, sed quia consequuntur magni dolores eos qui
ratione voluptatem sequi nesciunt.
</p>
<p className="mb-4">
Neque porro quisquam est, qui dolorem ipsum quia dolor sit
amet, consectetur, adipisci velit, sed quia non numquam eius
modi tempora incidunt ut labore et dolore magnam aliquam
quaerat voluptatem.
</p>
<p>
Ut enim ad minima veniam, quis nostrum exercitationem ullam
corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
consequatur.
</p>
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Many Tabs</h3>
<div className="h-[500px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="overview" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="overview">
Overview
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="analytics">
Analytics
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="reports">
Reports
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="notifications">
Notifications
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="integrations">
Integrations
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="billing">
Billing
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="overview">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
Dashboard Overview
</h4>
<p className="mb-4">
Dashboard overview with key metrics and recent activity.
</p>
<p className="mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna
aliqua.
</p>
<p>
Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="analytics">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Analytics</h4>
<p className="mb-4">
Detailed analytics and performance metrics.
</p>
<p className="mb-4">
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur.
</p>
<p>
Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="reports">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Reports</h4>
<p className="mb-4">
Generate and view reports for your account.
</p>
<p className="mb-4">
Sed ut perspiciatis unde omnis iste natus error sit
voluptatem accusantium doloremque laudantium.
</p>
<p>
Totam rem aperiam, eaque ipsa quae ab illo inventore
veritatis et quasi architecto beatae vitae dicta sunt
explicabo.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="notifications">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Notifications</h4>
<p className="mb-4">Manage your notification preferences.</p>
<p className="mb-4">
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit.
</p>
<p>
Sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="integrations">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Integrations</h4>
<p className="mb-4">
Connect and manage third-party integrations.
</p>
<p className="mb-4">
Neque porro quisquam est, qui dolorem ipsum quia dolor sit
amet.
</p>
<p>
Consectetur, adipisci velit, sed quia non numquam eius modi
tempora incidunt.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="billing">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Billing</h4>
<p className="mb-4">
View and manage your billing information.
</p>
<p className="mb-4">
Ut enim ad minima veniam, quis nostrum exercitationem ullam
corporis suscipit laboriosam.
</p>
<p>
Nisi ut aliquid ex ea commodi consequatur? Quis autem vel
eum iure reprehenderit qui in ea voluptate velit esse.
</p>
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
</div>
</div>
);
}
export const Default = {
render: () => <ScrollableTabsDemo />,
} satisfies Story;
export const ShortContent = {
render: () => (
<div className="p-8">
<div className="h-[200px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="account" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="account">
Account
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="password">
Password
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="account">
<div className="p-4 text-sm">
Make changes to your account here. Click save when you&apos;re
done.
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="password">
<div className="p-4 text-sm">
Change your password here. After saving, you&apos;ll be logged
out.
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
),
} satisfies Story;
export const LongContent = {
render: () => (
<div className="p-8">
<div className="h-[600px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="tab1" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="tab1">Account</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab2">Password</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab3">Settings</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="tab1">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Account Settings</h4>
<p className="mb-4">
Make changes to your account here. Click save when you&apos;re
done.
</p>
<p className="mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</p>
<p className="mb-4">
Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
<p className="mb-4">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
quae ab illo inventore veritatis et quasi architecto beatae
vitae dicta sunt explicabo.
</p>
<p>
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit
aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab2">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Password Settings</h4>
<p className="mb-4">
Change your password here. After saving, you&apos;ll be logged
out.
</p>
<p className="mb-4">
At vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos
dolores et quas molestias excepturi sint occaecati cupiditate
non provident.
</p>
<p className="mb-4">
Similique sunt in culpa qui officia deserunt mollitia animi, id
est laborum et dolorum fuga. Et harum quidem rerum facilis est
et expedita distinctio.
</p>
<p className="mb-4">
Nam libero tempore, cum soluta nobis est eligendi optio cumque
nihil impedit quo minus id quod maxime placeat facere possimus,
omnis voluptas assumenda est, omnis dolor repellendus.
</p>
<p>
Temporibus autem quibusdam et aut officiis debitis aut rerum
necessitatibus saepe eveniet ut et voluptates repudiandae sint
et molestiae non recusandae.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab3">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">General Settings</h4>
<p className="mb-4">Update your preferences and settings here.</p>
<p className="mb-4">
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi
tempora incidunt ut labore et dolore magnam aliquam quaerat
voluptatem.
</p>
<p className="mb-4">
Ut enim ad minima veniam, quis nostrum exercitationem ullam
corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
consequatur? Quis autem vel eum iure reprehenderit qui in ea
voluptate velit esse quam nihil molestiae consequatur.
</p>
<p className="mb-4">
Vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At
vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos
dolores.
</p>
<p>
Et quas molestias excepturi sint occaecati cupiditate non
provident, similique sunt in culpa qui officia deserunt mollitia
animi, id est laborum et dolorum fuga.
</p>
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
),
} satisfies Story;

View File

@@ -0,0 +1,59 @@
"use client";
import { cn } from "@/lib/utils";
import { Children } from "react";
import { ScrollableTabsContent } from "./components/ScrollableTabsContent";
import { ScrollableTabsList } from "./components/ScrollableTabsList";
import { ScrollableTabsTrigger } from "./components/ScrollableTabsTrigger";
import { ScrollableTabsContext } from "./context";
import { findContentElements, findListElement } from "./helpers";
import { useScrollableTabsInternal } from "./useScrollableTabs";
interface Props {
children?: React.ReactNode;
className?: string;
defaultValue?: string;
}
export function ScrollableTabs({ children, className, defaultValue }: Props) {
const {
activeValue,
setActiveValue,
registerContent,
scrollToSection,
scrollContainer,
contentContainerRef,
} = useScrollableTabsInternal({ defaultValue });
const childrenArray = Children.toArray(children);
const listElement = findListElement(childrenArray);
const contentElements = findContentElements(childrenArray);
return (
<ScrollableTabsContext.Provider
value={{
activeValue,
setActiveValue,
registerContent,
scrollToSection,
scrollContainer,
}}
>
<div className={cn("relative flex flex-col", className)}>
{listElement}
<div
ref={(node) => {
if (contentContainerRef) {
contentContainerRef.current = node;
}
}}
className="max-h-[64rem] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700"
>
<div className="min-h-full pb-[200px]">{contentElements}</div>
</div>
</div>
</ScrollableTabsContext.Provider>
);
}
export { ScrollableTabsContent, ScrollableTabsList, ScrollableTabsTrigger };

View File

@@ -0,0 +1,48 @@
"use client";
import { cn } from "@/lib/utils";
import * as React from "react";
import { useScrollableTabs } from "../context";
interface Props extends React.HTMLAttributes<HTMLDivElement> {
value: string;
}
export const ScrollableTabsContent = React.forwardRef<HTMLDivElement, Props>(
function ScrollableTabsContent(
{ className, value, children, ...props },
ref,
) {
const { registerContent } = useScrollableTabs();
const contentRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (contentRef.current) {
registerContent(value, contentRef.current);
}
return () => {
registerContent(value, null);
};
}, [value, registerContent]);
return (
<div
ref={(node) => {
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
contentRef.current = node;
}}
data-scrollable-tab-content
data-value={value}
className={cn("focus-visible:outline-none", className)}
{...props}
>
{children}
</div>
);
},
);
ScrollableTabsContent.displayName = "ScrollableTabsContent";

View File

@@ -0,0 +1,52 @@
"use client";
import { cn } from "@/lib/utils";
import * as React from "react";
import { useScrollableTabs } from "../context";
export const ScrollableTabsList = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(function ScrollableTabsList({ className, children, ...props }, ref) {
const { activeValue } = useScrollableTabs();
const [activeTabElement, setActiveTabElement] =
React.useState<HTMLElement | null>(null);
React.useEffect(() => {
const activeButton = Array.from(
document.querySelectorAll<HTMLElement>(
'[data-scrollable-tab-trigger][data-value="' + activeValue + '"]',
),
)[0];
if (activeButton) {
setActiveTabElement(activeButton);
}
}, [activeValue]);
return (
<div className="relative" ref={ref}>
<div
className={cn(
"inline-flex w-full items-center justify-start border-b border-zinc-100",
className,
)}
{...props}
>
{children}
</div>
{activeTabElement && (
<div
className="transition-left transition-right absolute bottom-0 h-0.5 bg-purple-600 duration-200 ease-in-out"
style={{
left: activeTabElement.offsetLeft,
width: activeTabElement.offsetWidth,
willChange: "left, width",
}}
/>
)}
</div>
);
});
ScrollableTabsList.displayName = "ScrollableTabsList";

Some files were not shown because too many files have changed in this diff Show More