mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 23:28:07 -05:00
Merge branch 'swiftyos/vector-search' of github.com:Significant-Gravitas/AutoGPT into swiftyos/vector-search
This commit is contained in:
@@ -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: >
|
||||
|
||||
2
.github/workflows/repo-pr-label.yml
vendored
2
.github/workflows/repo-pr-label.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
342
autogpt_platform/frontend/pnpm-lock.yaml
generated
342
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'll show up here.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 "Set up
|
||||
trigger" 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 "Set up
|
||||
trigger" 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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={[
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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're
|
||||
done.
|
||||
</div>
|
||||
</ScrollableTabsContent>
|
||||
<ScrollableTabsContent value="tab2">
|
||||
<div className="p-4 text-sm">
|
||||
Change your password here. After saving, you'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'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'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're
|
||||
done.
|
||||
</div>
|
||||
</ScrollableTabsContent>
|
||||
<ScrollableTabsContent value="password">
|
||||
<div className="p-4 text-sm">
|
||||
Change your password here. After saving, you'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'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'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;
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
@@ -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
Reference in New Issue
Block a user