mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-14 09:38:00 -05:00
Compare commits
17 Commits
dev
...
claude-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88532185bc | ||
|
|
3a56343013 | ||
|
|
b02b5e0708 | ||
|
|
1239cb7a4d | ||
|
|
6bc05de917 | ||
|
|
da04bcbea5 | ||
|
|
34c1644bd6 | ||
|
|
6220396cd7 | ||
|
|
75a92fc3f3 | ||
|
|
2c6353a6a2 | ||
|
|
21b70ae9ae | ||
|
|
b007e02364 | ||
|
|
bbc289894a | ||
|
|
639ee5f073 | ||
|
|
898781134d | ||
|
|
7a28db1649 | ||
|
|
a916ea0f8f |
631
autogpt_platform/backend/backend/blocks/claude_code.py
Normal file
631
autogpt_platform/backend/backend/blocks/claude_code.py
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
import json
|
||||||
|
import shlex
|
||||||
|
import uuid
|
||||||
|
from typing import Literal, Optional
|
||||||
|
|
||||||
|
from e2b import AsyncSandbox as BaseAsyncSandbox
|
||||||
|
from pydantic import BaseModel, SecretStr
|
||||||
|
|
||||||
|
from backend.data.block import (
|
||||||
|
Block,
|
||||||
|
BlockCategory,
|
||||||
|
BlockOutput,
|
||||||
|
BlockSchemaInput,
|
||||||
|
BlockSchemaOutput,
|
||||||
|
)
|
||||||
|
from backend.data.model import (
|
||||||
|
APIKeyCredentials,
|
||||||
|
CredentialsField,
|
||||||
|
CredentialsMetaInput,
|
||||||
|
SchemaField,
|
||||||
|
)
|
||||||
|
from backend.integrations.providers import ProviderName
|
||||||
|
|
||||||
|
# Test credentials for E2B
|
||||||
|
TEST_E2B_CREDENTIALS = APIKeyCredentials(
|
||||||
|
id="01234567-89ab-cdef-0123-456789abcdef",
|
||||||
|
provider="e2b",
|
||||||
|
api_key=SecretStr("mock-e2b-api-key"),
|
||||||
|
title="Mock E2B API key",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
TEST_E2B_CREDENTIALS_INPUT = {
|
||||||
|
"provider": TEST_E2B_CREDENTIALS.provider,
|
||||||
|
"id": TEST_E2B_CREDENTIALS.id,
|
||||||
|
"type": TEST_E2B_CREDENTIALS.type,
|
||||||
|
"title": TEST_E2B_CREDENTIALS.title,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test credentials for Anthropic
|
||||||
|
TEST_ANTHROPIC_CREDENTIALS = APIKeyCredentials(
|
||||||
|
id="2e568a2b-b2ea-475a-8564-9a676bf31c56",
|
||||||
|
provider="anthropic",
|
||||||
|
api_key=SecretStr("mock-anthropic-api-key"),
|
||||||
|
title="Mock Anthropic API key",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
TEST_ANTHROPIC_CREDENTIALS_INPUT = {
|
||||||
|
"provider": TEST_ANTHROPIC_CREDENTIALS.provider,
|
||||||
|
"id": TEST_ANTHROPIC_CREDENTIALS.id,
|
||||||
|
"type": TEST_ANTHROPIC_CREDENTIALS.type,
|
||||||
|
"title": TEST_ANTHROPIC_CREDENTIALS.title,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ClaudeCodeBlock(Block):
|
||||||
|
"""
|
||||||
|
Execute tasks using Claude Code (Anthropic's AI coding assistant) in an E2B sandbox.
|
||||||
|
|
||||||
|
Claude Code can create files, install tools, run commands, and perform complex
|
||||||
|
coding tasks autonomously within a secure sandbox environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Use base template - we'll install Claude Code ourselves for latest version
|
||||||
|
DEFAULT_TEMPLATE = "base"
|
||||||
|
|
||||||
|
class Input(BlockSchemaInput):
|
||||||
|
e2b_credentials: CredentialsMetaInput[
|
||||||
|
Literal[ProviderName.E2B], Literal["api_key"]
|
||||||
|
] = CredentialsField(
|
||||||
|
description=(
|
||||||
|
"API key for the E2B platform to create the sandbox. "
|
||||||
|
"Get one on the [e2b website](https://e2b.dev/docs)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
anthropic_credentials: CredentialsMetaInput[
|
||||||
|
Literal[ProviderName.ANTHROPIC], Literal["api_key"]
|
||||||
|
] = CredentialsField(
|
||||||
|
description=(
|
||||||
|
"API key for Anthropic to power Claude Code. "
|
||||||
|
"Get one at [Anthropic's website](https://console.anthropic.com)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt: str = SchemaField(
|
||||||
|
description=(
|
||||||
|
"The task or instruction for Claude Code to execute. "
|
||||||
|
"Claude Code can create files, install packages, run commands, "
|
||||||
|
"and perform complex coding tasks."
|
||||||
|
),
|
||||||
|
placeholder="Create a hello world index.html file",
|
||||||
|
default="",
|
||||||
|
advanced=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
timeout: int = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Sandbox timeout in seconds. Claude Code tasks can take "
|
||||||
|
"a while, so set this appropriately for your task complexity. "
|
||||||
|
"Note: This only applies when creating a new sandbox. "
|
||||||
|
"When reconnecting to an existing sandbox via sandbox_id, "
|
||||||
|
"the original timeout is retained."
|
||||||
|
),
|
||||||
|
default=300, # 5 minutes default
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_commands: list[str] = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Optional shell commands to run before executing Claude Code. "
|
||||||
|
"Useful for installing dependencies or setting up the environment."
|
||||||
|
),
|
||||||
|
default_factory=list,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
working_directory: str = SchemaField(
|
||||||
|
description="Working directory for Claude Code to operate in.",
|
||||||
|
default="/home/user",
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Session/continuation support
|
||||||
|
session_id: str = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Session ID to resume a previous conversation. "
|
||||||
|
"Leave empty for a new conversation. "
|
||||||
|
"Use the session_id from a previous run to continue that conversation."
|
||||||
|
),
|
||||||
|
default="",
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
sandbox_id: str = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Sandbox ID to reconnect to an existing sandbox. "
|
||||||
|
"Required when resuming a session (along with session_id). "
|
||||||
|
"Use the sandbox_id from a previous run where dispose_sandbox was False."
|
||||||
|
),
|
||||||
|
default="",
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation_history: str = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Previous conversation history to continue from. "
|
||||||
|
"Use this to restore context on a fresh sandbox if the previous one timed out. "
|
||||||
|
"Pass the conversation_history output from a previous run."
|
||||||
|
),
|
||||||
|
default="",
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispose_sandbox: bool = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Whether to dispose of the sandbox immediately after execution. "
|
||||||
|
"Set to False if you want to continue the conversation later "
|
||||||
|
"(you'll need both sandbox_id and session_id from the output)."
|
||||||
|
),
|
||||||
|
default=True,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FileOutput(BaseModel):
|
||||||
|
"""A file extracted from the sandbox."""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
relative_path: str # Path relative to working directory (for GitHub, etc.)
|
||||||
|
name: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
class Output(BlockSchemaOutput):
|
||||||
|
response: str = SchemaField(
|
||||||
|
description="The output/response from Claude Code execution"
|
||||||
|
)
|
||||||
|
files: list["ClaudeCodeBlock.FileOutput"] = SchemaField(
|
||||||
|
description=(
|
||||||
|
"List of text files created/modified by Claude Code during this execution. "
|
||||||
|
"Each file has 'path', 'relative_path', 'name', and 'content' fields."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conversation_history: str = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Full conversation history including this turn. "
|
||||||
|
"Pass this to conversation_history input to continue on a fresh sandbox "
|
||||||
|
"if the previous sandbox timed out."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session_id: str = SchemaField(
|
||||||
|
description=(
|
||||||
|
"Session ID for this conversation. "
|
||||||
|
"Pass this back along with sandbox_id to continue the conversation."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sandbox_id: Optional[str] = SchemaField(
|
||||||
|
description=(
|
||||||
|
"ID of the sandbox instance. "
|
||||||
|
"Pass this back along with session_id to continue the conversation. "
|
||||||
|
"This is None if dispose_sandbox was True (sandbox was disposed)."
|
||||||
|
),
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
error: str = SchemaField(description="Error message if execution failed")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="4e34f4a5-9b89-4326-ba77-2dd6750b7194",
|
||||||
|
description=(
|
||||||
|
"Execute tasks using Claude Code in an E2B sandbox. "
|
||||||
|
"Claude Code can create files, install tools, run commands, "
|
||||||
|
"and perform complex coding tasks autonomously."
|
||||||
|
),
|
||||||
|
categories={BlockCategory.DEVELOPER_TOOLS, BlockCategory.AI},
|
||||||
|
input_schema=ClaudeCodeBlock.Input,
|
||||||
|
output_schema=ClaudeCodeBlock.Output,
|
||||||
|
test_credentials={
|
||||||
|
"e2b_credentials": TEST_E2B_CREDENTIALS,
|
||||||
|
"anthropic_credentials": TEST_ANTHROPIC_CREDENTIALS,
|
||||||
|
},
|
||||||
|
test_input={
|
||||||
|
"e2b_credentials": TEST_E2B_CREDENTIALS_INPUT,
|
||||||
|
"anthropic_credentials": TEST_ANTHROPIC_CREDENTIALS_INPUT,
|
||||||
|
"prompt": "Create a hello world HTML file",
|
||||||
|
"timeout": 300,
|
||||||
|
"setup_commands": [],
|
||||||
|
"working_directory": "/home/user",
|
||||||
|
"session_id": "",
|
||||||
|
"sandbox_id": "",
|
||||||
|
"conversation_history": "",
|
||||||
|
"dispose_sandbox": True,
|
||||||
|
},
|
||||||
|
test_output=[
|
||||||
|
("response", "Created index.html with hello world content"),
|
||||||
|
(
|
||||||
|
"files",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "/home/user/index.html",
|
||||||
|
"relative_path": "index.html",
|
||||||
|
"name": "index.html",
|
||||||
|
"content": "<html>Hello World</html>",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"conversation_history",
|
||||||
|
"User: Create a hello world HTML file\n"
|
||||||
|
"Claude: Created index.html with hello world content",
|
||||||
|
),
|
||||||
|
("session_id", str),
|
||||||
|
("sandbox_id", None), # None because dispose_sandbox=True in test_input
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"execute_claude_code": lambda *args, **kwargs: (
|
||||||
|
"Created index.html with hello world content", # response
|
||||||
|
[
|
||||||
|
ClaudeCodeBlock.FileOutput(
|
||||||
|
path="/home/user/index.html",
|
||||||
|
relative_path="index.html",
|
||||||
|
name="index.html",
|
||||||
|
content="<html>Hello World</html>",
|
||||||
|
)
|
||||||
|
], # files
|
||||||
|
"User: Create a hello world HTML file\n"
|
||||||
|
"Claude: Created index.html with hello world content", # conversation_history
|
||||||
|
"test-session-id", # session_id
|
||||||
|
"sandbox_id", # sandbox_id
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def execute_claude_code(
|
||||||
|
self,
|
||||||
|
e2b_api_key: str,
|
||||||
|
anthropic_api_key: str,
|
||||||
|
prompt: str,
|
||||||
|
timeout: int,
|
||||||
|
setup_commands: list[str],
|
||||||
|
working_directory: str,
|
||||||
|
session_id: str,
|
||||||
|
existing_sandbox_id: str,
|
||||||
|
conversation_history: str,
|
||||||
|
dispose_sandbox: bool,
|
||||||
|
) -> tuple[str, list["ClaudeCodeBlock.FileOutput"], str, str, str]:
|
||||||
|
"""
|
||||||
|
Execute Claude Code in an E2B sandbox.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (response, files, conversation_history, session_id, sandbox_id)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Validate that sandbox_id is provided when resuming a session
|
||||||
|
if session_id and not existing_sandbox_id:
|
||||||
|
raise ValueError(
|
||||||
|
"sandbox_id is required when resuming a session with session_id. "
|
||||||
|
"The session state is stored in the original sandbox. "
|
||||||
|
"If the sandbox has timed out, use conversation_history instead "
|
||||||
|
"to restore context on a fresh sandbox."
|
||||||
|
)
|
||||||
|
|
||||||
|
sandbox = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Either reconnect to existing sandbox or create a new one
|
||||||
|
if existing_sandbox_id:
|
||||||
|
# Reconnect to existing sandbox for conversation continuation
|
||||||
|
sandbox = await BaseAsyncSandbox.connect(
|
||||||
|
sandbox_id=existing_sandbox_id,
|
||||||
|
api_key=e2b_api_key,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Create new sandbox
|
||||||
|
sandbox = await BaseAsyncSandbox.create(
|
||||||
|
template=self.DEFAULT_TEMPLATE,
|
||||||
|
api_key=e2b_api_key,
|
||||||
|
timeout=timeout,
|
||||||
|
envs={"ANTHROPIC_API_KEY": anthropic_api_key},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install Claude Code from npm (ensures we get the latest version)
|
||||||
|
install_result = await sandbox.commands.run(
|
||||||
|
"npm install -g @anthropic-ai/claude-code@latest",
|
||||||
|
timeout=120, # 2 min timeout for install
|
||||||
|
)
|
||||||
|
if install_result.exit_code != 0:
|
||||||
|
raise Exception(
|
||||||
|
f"Failed to install Claude Code: {install_result.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run any user-provided setup commands
|
||||||
|
for cmd in setup_commands:
|
||||||
|
setup_result = await sandbox.commands.run(cmd)
|
||||||
|
if setup_result.exit_code != 0:
|
||||||
|
raise Exception(
|
||||||
|
f"Setup command failed: {cmd}\n"
|
||||||
|
f"Exit code: {setup_result.exit_code}\n"
|
||||||
|
f"Stdout: {setup_result.stdout}\n"
|
||||||
|
f"Stderr: {setup_result.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate or use provided session ID
|
||||||
|
current_session_id = session_id if session_id else str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Build base Claude flags
|
||||||
|
base_flags = "-p --dangerously-skip-permissions --output-format json"
|
||||||
|
|
||||||
|
# Add conversation history context if provided (for fresh sandbox continuation)
|
||||||
|
history_flag = ""
|
||||||
|
if conversation_history and not session_id:
|
||||||
|
# Inject previous conversation as context via system prompt
|
||||||
|
# Use consistent escaping via _escape_prompt helper
|
||||||
|
escaped_history = self._escape_prompt(
|
||||||
|
f"Previous conversation context: {conversation_history}"
|
||||||
|
)
|
||||||
|
history_flag = f" --append-system-prompt {escaped_history}"
|
||||||
|
|
||||||
|
# Build Claude command based on whether we're resuming or starting new
|
||||||
|
# Use shlex.quote for working_directory and session IDs to prevent injection
|
||||||
|
safe_working_dir = shlex.quote(working_directory)
|
||||||
|
if session_id:
|
||||||
|
# Resuming existing session (sandbox still alive)
|
||||||
|
safe_session_id = shlex.quote(session_id)
|
||||||
|
claude_command = (
|
||||||
|
f"cd {safe_working_dir} && "
|
||||||
|
f"echo {self._escape_prompt(prompt)} | "
|
||||||
|
f"claude --resume {safe_session_id} {base_flags}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# New session with specific ID
|
||||||
|
safe_current_session_id = shlex.quote(current_session_id)
|
||||||
|
claude_command = (
|
||||||
|
f"cd {safe_working_dir} && "
|
||||||
|
f"echo {self._escape_prompt(prompt)} | "
|
||||||
|
f"claude --session-id {safe_current_session_id} {base_flags}{history_flag}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capture timestamp before running Claude Code to filter files later
|
||||||
|
# Capture timestamp 1 second in the past to avoid race condition with file creation
|
||||||
|
timestamp_result = await sandbox.commands.run(
|
||||||
|
"date -u -d '1 second ago' +%Y-%m-%dT%H:%M:%S"
|
||||||
|
)
|
||||||
|
if timestamp_result.exit_code != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to capture timestamp: {timestamp_result.stderr}"
|
||||||
|
)
|
||||||
|
start_timestamp = (
|
||||||
|
timestamp_result.stdout.strip() if timestamp_result.stdout else None
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await sandbox.commands.run(
|
||||||
|
claude_command,
|
||||||
|
timeout=0, # No command timeout - let sandbox timeout handle it
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for command failure
|
||||||
|
if result.exit_code != 0:
|
||||||
|
error_msg = result.stderr or result.stdout or "Unknown error"
|
||||||
|
raise Exception(
|
||||||
|
f"Claude Code command failed with exit code {result.exit_code}:\n"
|
||||||
|
f"{error_msg}"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_output = result.stdout or ""
|
||||||
|
sandbox_id = sandbox.sandbox_id
|
||||||
|
|
||||||
|
# Parse JSON output to extract response and build conversation history
|
||||||
|
response = ""
|
||||||
|
new_conversation_history = conversation_history or ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# The JSON output contains the result
|
||||||
|
output_data = json.loads(raw_output)
|
||||||
|
response = output_data.get("result", raw_output)
|
||||||
|
|
||||||
|
# Build conversation history entry
|
||||||
|
turn_entry = f"User: {prompt}\nClaude: {response}"
|
||||||
|
if new_conversation_history:
|
||||||
|
new_conversation_history = (
|
||||||
|
f"{new_conversation_history}\n\n{turn_entry}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_conversation_history = turn_entry
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# If not valid JSON, use raw output
|
||||||
|
response = raw_output
|
||||||
|
turn_entry = f"User: {prompt}\nClaude: {response}"
|
||||||
|
if new_conversation_history:
|
||||||
|
new_conversation_history = (
|
||||||
|
f"{new_conversation_history}\n\n{turn_entry}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_conversation_history = turn_entry
|
||||||
|
|
||||||
|
# Extract files created/modified during this run
|
||||||
|
files = await self._extract_files(
|
||||||
|
sandbox, working_directory, start_timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
response,
|
||||||
|
files,
|
||||||
|
new_conversation_history,
|
||||||
|
current_session_id,
|
||||||
|
sandbox_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if dispose_sandbox and sandbox:
|
||||||
|
await sandbox.kill()
|
||||||
|
|
||||||
|
async def _extract_files(
|
||||||
|
self,
|
||||||
|
sandbox: BaseAsyncSandbox,
|
||||||
|
working_directory: str,
|
||||||
|
since_timestamp: str | None = None,
|
||||||
|
) -> list["ClaudeCodeBlock.FileOutput"]:
|
||||||
|
"""
|
||||||
|
Extract text files created/modified during this Claude Code execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sandbox: The E2B sandbox instance
|
||||||
|
working_directory: Directory to search for files
|
||||||
|
since_timestamp: ISO timestamp - only return files modified after this time
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of FileOutput objects with path, relative_path, name, and content
|
||||||
|
"""
|
||||||
|
files: list[ClaudeCodeBlock.FileOutput] = []
|
||||||
|
|
||||||
|
# Text file extensions we can safely read as text
|
||||||
|
text_extensions = {
|
||||||
|
".txt",
|
||||||
|
".md",
|
||||||
|
".html",
|
||||||
|
".htm",
|
||||||
|
".css",
|
||||||
|
".js",
|
||||||
|
".ts",
|
||||||
|
".jsx",
|
||||||
|
".tsx",
|
||||||
|
".json",
|
||||||
|
".xml",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".toml",
|
||||||
|
".ini",
|
||||||
|
".cfg",
|
||||||
|
".conf",
|
||||||
|
".py",
|
||||||
|
".rb",
|
||||||
|
".php",
|
||||||
|
".java",
|
||||||
|
".c",
|
||||||
|
".cpp",
|
||||||
|
".h",
|
||||||
|
".hpp",
|
||||||
|
".cs",
|
||||||
|
".go",
|
||||||
|
".rs",
|
||||||
|
".swift",
|
||||||
|
".kt",
|
||||||
|
".scala",
|
||||||
|
".sh",
|
||||||
|
".bash",
|
||||||
|
".zsh",
|
||||||
|
".sql",
|
||||||
|
".graphql",
|
||||||
|
".env",
|
||||||
|
".gitignore",
|
||||||
|
".dockerfile",
|
||||||
|
"Dockerfile",
|
||||||
|
".vue",
|
||||||
|
".svelte",
|
||||||
|
".astro",
|
||||||
|
".mdx",
|
||||||
|
".rst",
|
||||||
|
".tex",
|
||||||
|
".csv",
|
||||||
|
".log",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# List files recursively using find command
|
||||||
|
# Exclude node_modules and .git directories, but allow hidden files
|
||||||
|
# like .env and .gitignore (they're filtered by text_extensions later)
|
||||||
|
# Filter by timestamp to only get files created/modified during this run
|
||||||
|
safe_working_dir = shlex.quote(working_directory)
|
||||||
|
timestamp_filter = ""
|
||||||
|
if since_timestamp:
|
||||||
|
timestamp_filter = f"-newermt {shlex.quote(since_timestamp)} "
|
||||||
|
find_result = await sandbox.commands.run(
|
||||||
|
f"find {safe_working_dir} -type f "
|
||||||
|
f"{timestamp_filter}"
|
||||||
|
f"-not -path '*/node_modules/*' "
|
||||||
|
f"-not -path '*/.git/*' "
|
||||||
|
f"2>/dev/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
if find_result.stdout:
|
||||||
|
for file_path in find_result.stdout.strip().split("\n"):
|
||||||
|
if not file_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if it's a text file we can read
|
||||||
|
is_text = any(
|
||||||
|
file_path.endswith(ext) for ext in text_extensions
|
||||||
|
) or file_path.endswith("Dockerfile")
|
||||||
|
|
||||||
|
if is_text:
|
||||||
|
try:
|
||||||
|
content = await sandbox.files.read(file_path)
|
||||||
|
# Handle bytes or string
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
content = content.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
# Extract filename from path
|
||||||
|
file_name = file_path.split("/")[-1]
|
||||||
|
|
||||||
|
# Calculate relative path by stripping working directory
|
||||||
|
relative_path = file_path
|
||||||
|
if file_path.startswith(working_directory):
|
||||||
|
relative_path = file_path[len(working_directory) :]
|
||||||
|
# Remove leading slash if present
|
||||||
|
if relative_path.startswith("/"):
|
||||||
|
relative_path = relative_path[1:]
|
||||||
|
|
||||||
|
files.append(
|
||||||
|
ClaudeCodeBlock.FileOutput(
|
||||||
|
path=file_path,
|
||||||
|
relative_path=relative_path,
|
||||||
|
name=file_name,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# Skip files that can't be read
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# If file extraction fails, return empty results
|
||||||
|
pass
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
def _escape_prompt(self, prompt: str) -> str:
|
||||||
|
"""Escape the prompt for safe shell execution."""
|
||||||
|
# Use single quotes and escape any single quotes in the prompt
|
||||||
|
escaped = prompt.replace("'", "'\"'\"'")
|
||||||
|
return f"'{escaped}'"
|
||||||
|
|
||||||
|
async def run(
|
||||||
|
self,
|
||||||
|
input_data: Input,
|
||||||
|
*,
|
||||||
|
e2b_credentials: APIKeyCredentials,
|
||||||
|
anthropic_credentials: APIKeyCredentials,
|
||||||
|
**kwargs,
|
||||||
|
) -> BlockOutput:
|
||||||
|
try:
|
||||||
|
(
|
||||||
|
response,
|
||||||
|
files,
|
||||||
|
conversation_history,
|
||||||
|
session_id,
|
||||||
|
sandbox_id,
|
||||||
|
) = await self.execute_claude_code(
|
||||||
|
e2b_api_key=e2b_credentials.api_key.get_secret_value(),
|
||||||
|
anthropic_api_key=anthropic_credentials.api_key.get_secret_value(),
|
||||||
|
prompt=input_data.prompt,
|
||||||
|
timeout=input_data.timeout,
|
||||||
|
setup_commands=input_data.setup_commands,
|
||||||
|
working_directory=input_data.working_directory,
|
||||||
|
session_id=input_data.session_id,
|
||||||
|
existing_sandbox_id=input_data.sandbox_id,
|
||||||
|
conversation_history=input_data.conversation_history,
|
||||||
|
dispose_sandbox=input_data.dispose_sandbox,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield "response", response
|
||||||
|
# Always yield files (empty list if none) to match Output schema
|
||||||
|
yield "files", [f.model_dump() for f in files]
|
||||||
|
# Always yield conversation_history so user can restore context on fresh sandbox
|
||||||
|
yield "conversation_history", conversation_history
|
||||||
|
# Always yield session_id so user can continue conversation
|
||||||
|
yield "session_id", session_id
|
||||||
|
# Always yield sandbox_id (None if disposed) to match Output schema
|
||||||
|
yield "sandbox_id", sandbox_id if not input_data.dispose_sandbox else None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
yield "error", str(e)
|
||||||
@@ -81,16 +81,18 @@ export const RunInputDialog = ({
|
|||||||
Inputs
|
Inputs
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<FormRenderer
|
<div className="px-2">
|
||||||
jsonSchema={inputSchema as RJSFSchema}
|
<FormRenderer
|
||||||
handleChange={(v) => handleInputChange(v.formData)}
|
jsonSchema={inputSchema as RJSFSchema}
|
||||||
uiSchema={uiSchema}
|
handleChange={(v) => handleInputChange(v.formData)}
|
||||||
initialValues={{}}
|
uiSchema={uiSchema}
|
||||||
formContext={{
|
initialValues={{}}
|
||||||
showHandles: false,
|
formContext={{
|
||||||
size: "large",
|
showHandles: false,
|
||||||
}}
|
size: "large",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/def
|
|||||||
import {
|
import {
|
||||||
useGetV1GetExecutionDetails,
|
useGetV1GetExecutionDetails,
|
||||||
useGetV1GetSpecificGraph,
|
useGetV1GetSpecificGraph,
|
||||||
useGetV1ListUserGraphs,
|
|
||||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||||
@@ -18,7 +17,6 @@ import { useReactFlow } from "@xyflow/react";
|
|||||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||||
import { useHistoryStore } from "../../../stores/historyStore";
|
import { useHistoryStore } from "../../../stores/historyStore";
|
||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
import { okData } from "@/app/api/helpers";
|
|
||||||
|
|
||||||
export const useFlow = () => {
|
export const useFlow = () => {
|
||||||
const [isLocked, setIsLocked] = useState(false);
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
@@ -38,9 +36,6 @@ export const useFlow = () => {
|
|||||||
const setGraphExecutionStatus = useGraphStore(
|
const setGraphExecutionStatus = useGraphStore(
|
||||||
useShallow((state) => state.setGraphExecutionStatus),
|
useShallow((state) => state.setGraphExecutionStatus),
|
||||||
);
|
);
|
||||||
const setAvailableSubGraphs = useGraphStore(
|
|
||||||
useShallow((state) => state.setAvailableSubGraphs),
|
|
||||||
);
|
|
||||||
const updateEdgeBeads = useEdgeStore(
|
const updateEdgeBeads = useEdgeStore(
|
||||||
useShallow((state) => state.updateEdgeBeads),
|
useShallow((state) => state.updateEdgeBeads),
|
||||||
);
|
);
|
||||||
@@ -67,11 +62,6 @@ export const useFlow = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch all available graphs for sub-agent update detection
|
|
||||||
const { data: availableGraphs } = useGetV1ListUserGraphs({
|
|
||||||
query: { select: okData },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
|
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
|
||||||
flowID ?? "",
|
flowID ?? "",
|
||||||
flowVersion !== null ? { version: flowVersion } : {},
|
flowVersion !== null ? { version: flowVersion } : {},
|
||||||
@@ -126,18 +116,10 @@ export const useFlow = () => {
|
|||||||
}
|
}
|
||||||
}, [graph]);
|
}, [graph]);
|
||||||
|
|
||||||
// Update available sub-graphs in store for sub-agent update detection
|
|
||||||
useEffect(() => {
|
|
||||||
if (availableGraphs) {
|
|
||||||
setAvailableSubGraphs(availableGraphs);
|
|
||||||
}
|
|
||||||
}, [availableGraphs, setAvailableSubGraphs]);
|
|
||||||
|
|
||||||
// adding nodes
|
// adding nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customNodes.length > 0) {
|
if (customNodes.length > 0) {
|
||||||
useNodeStore.getState().setNodes([]);
|
useNodeStore.getState().setNodes([]);
|
||||||
useNodeStore.getState().clearResolutionState();
|
|
||||||
addNodes(customNodes);
|
addNodes(customNodes);
|
||||||
|
|
||||||
// Sync hardcoded values with handle IDs.
|
// Sync hardcoded values with handle IDs.
|
||||||
@@ -221,7 +203,6 @@ export const useFlow = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
useNodeStore.getState().setNodes([]);
|
useNodeStore.getState().setNodes([]);
|
||||||
useNodeStore.getState().clearResolutionState();
|
|
||||||
useEdgeStore.getState().setEdges([]);
|
useEdgeStore.getState().setEdges([]);
|
||||||
useGraphStore.getState().reset();
|
useGraphStore.getState().reset();
|
||||||
useEdgeStore.getState().resetEdgeBeads();
|
useEdgeStore.getState().resetEdgeBeads();
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
getBezierPath,
|
getBezierPath,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
import { XIcon } from "@phosphor-icons/react";
|
import { XIcon } from "@phosphor-icons/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
|
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
|
||||||
@@ -36,8 +35,6 @@ const CustomEdge = ({
|
|||||||
selected,
|
selected,
|
||||||
}: EdgeProps<CustomEdge>) => {
|
}: EdgeProps<CustomEdge>) => {
|
||||||
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
||||||
// Subscribe to the brokenEdgeIDs map and check if this edge is broken across any node
|
|
||||||
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getBezierPath({
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
@@ -53,12 +50,6 @@ const CustomEdge = ({
|
|||||||
const beadUp = data?.beadUp ?? 0;
|
const beadUp = data?.beadUp ?? 0;
|
||||||
const beadDown = data?.beadDown ?? 0;
|
const beadDown = data?.beadDown ?? 0;
|
||||||
|
|
||||||
const handleRemoveEdge = () => {
|
|
||||||
removeConnection(id);
|
|
||||||
// Note: broken edge tracking is cleaned up automatically by useSubAgentUpdateState
|
|
||||||
// when it detects the edge no longer exists
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
@@ -66,11 +57,9 @@ const CustomEdge = ({
|
|||||||
markerEnd={markerEnd}
|
markerEnd={markerEnd}
|
||||||
className={cn(
|
className={cn(
|
||||||
isStatic && "!stroke-[1.5px] [stroke-dasharray:6]",
|
isStatic && "!stroke-[1.5px] [stroke-dasharray:6]",
|
||||||
isBroken
|
selected
|
||||||
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
? "stroke-zinc-800"
|
||||||
: selected
|
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
||||||
? "stroke-zinc-800"
|
|
||||||
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<JSBeads
|
<JSBeads
|
||||||
@@ -81,16 +70,12 @@ const CustomEdge = ({
|
|||||||
/>
|
/>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRemoveEdge}
|
onClick={() => removeConnection(id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-fit min-w-0 p-1 transition-opacity",
|
"absolute h-fit min-w-0 p-1 transition-opacity",
|
||||||
isBroken
|
isHovered ? "opacity-100" : "opacity-0",
|
||||||
? "bg-red-500 opacity-100 hover:bg-red-600"
|
|
||||||
: isHovered
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
)}
|
||||||
variant={isBroken ? "primary" : "secondary"}
|
variant="secondary"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Handle, Position } from "@xyflow/react";
|
|||||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useNodeStore } from "../../../stores/nodeStore";
|
|
||||||
|
|
||||||
const InputNodeHandle = ({
|
const InputNodeHandle = ({
|
||||||
handleId,
|
handleId,
|
||||||
@@ -16,9 +15,6 @@ const InputNodeHandle = ({
|
|||||||
const isInputConnected = useEdgeStore((state) =>
|
const isInputConnected = useEdgeStore((state) =>
|
||||||
state.isInputConnected(nodeId ?? "", cleanedHandleId),
|
state.isInputConnected(nodeId ?? "", cleanedHandleId),
|
||||||
);
|
);
|
||||||
const isInputBroken = useNodeStore((state) =>
|
|
||||||
state.isInputBroken(nodeId, cleanedHandleId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Handle
|
<Handle
|
||||||
@@ -31,10 +27,7 @@ const InputNodeHandle = ({
|
|||||||
<CircleIcon
|
<CircleIcon
|
||||||
size={16}
|
size={16}
|
||||||
weight={isInputConnected ? "fill" : "duotone"}
|
weight={isInputConnected ? "fill" : "duotone"}
|
||||||
className={cn(
|
className={"text-gray-400 opacity-100"}
|
||||||
"text-gray-400 opacity-100",
|
|
||||||
isInputBroken && "text-red-500",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
@@ -45,17 +38,14 @@ const OutputNodeHandle = ({
|
|||||||
field_name,
|
field_name,
|
||||||
nodeId,
|
nodeId,
|
||||||
hexColor,
|
hexColor,
|
||||||
isBroken,
|
|
||||||
}: {
|
}: {
|
||||||
field_name: string;
|
field_name: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
hexColor: string;
|
hexColor: string;
|
||||||
isBroken: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const isOutputConnected = useEdgeStore((state) =>
|
const isOutputConnected = useEdgeStore((state) =>
|
||||||
state.isOutputConnected(nodeId, field_name),
|
state.isOutputConnected(nodeId, field_name),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Handle
|
<Handle
|
||||||
type={"source"}
|
type={"source"}
|
||||||
@@ -68,10 +58,7 @@ const OutputNodeHandle = ({
|
|||||||
size={16}
|
size={16}
|
||||||
weight={"duotone"}
|
weight={"duotone"}
|
||||||
color={isOutputConnected ? hexColor : "gray"}
|
color={isOutputConnected ? hexColor : "gray"}
|
||||||
className={cn(
|
className={cn("text-gray-400 opacity-100")}
|
||||||
"text-gray-400 opacity-100",
|
|
||||||
isBroken && "text-red-500",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
|||||||
import { NodeRightClickMenu } from "./components/NodeRightClickMenu";
|
import { NodeRightClickMenu } from "./components/NodeRightClickMenu";
|
||||||
import { StickyNoteBlock } from "./components/StickyNoteBlock";
|
import { StickyNoteBlock } from "./components/StickyNoteBlock";
|
||||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||||
import { SubAgentUpdateFeature } from "./components/SubAgentUpdate/SubAgentUpdateFeature";
|
|
||||||
import { useCustomNode } from "./useCustomNode";
|
|
||||||
|
|
||||||
export type CustomNodeData = {
|
export type CustomNodeData = {
|
||||||
hardcodedValues: {
|
hardcodedValues: {
|
||||||
@@ -47,10 +45,6 @@ export type CustomNode = XYNode<CustomNodeData, "custom">;
|
|||||||
|
|
||||||
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||||
({ data, id: nodeId, selected }) => {
|
({ data, id: nodeId, selected }) => {
|
||||||
const { inputSchema, outputSchema } = useCustomNode({ data, nodeId });
|
|
||||||
|
|
||||||
const isAgent = data.uiType === BlockUIType.AGENT;
|
|
||||||
|
|
||||||
if (data.uiType === BlockUIType.NOTE) {
|
if (data.uiType === BlockUIType.NOTE) {
|
||||||
return (
|
return (
|
||||||
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
|
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
|
||||||
@@ -69,6 +63,16 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
|||||||
|
|
||||||
const isAyrshare = data.uiType === BlockUIType.AYRSHARE;
|
const isAyrshare = data.uiType === BlockUIType.AYRSHARE;
|
||||||
|
|
||||||
|
const inputSchema =
|
||||||
|
data.uiType === BlockUIType.AGENT
|
||||||
|
? (data.hardcodedValues.input_schema ?? {})
|
||||||
|
: data.inputSchema;
|
||||||
|
|
||||||
|
const outputSchema =
|
||||||
|
data.uiType === BlockUIType.AGENT
|
||||||
|
? (data.hardcodedValues.output_schema ?? {})
|
||||||
|
: data.outputSchema;
|
||||||
|
|
||||||
const hasConfigErrors =
|
const hasConfigErrors =
|
||||||
data.errors &&
|
data.errors &&
|
||||||
Object.values(data.errors).some(
|
Object.values(data.errors).some(
|
||||||
@@ -83,11 +87,12 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
|||||||
|
|
||||||
const hasErrors = hasConfigErrors || hasOutputError;
|
const hasErrors = hasConfigErrors || hasOutputError;
|
||||||
|
|
||||||
|
// Currently all blockTypes design are similar - that's why i am using the same component for all of them
|
||||||
|
// If in future - if we need some drastic change in some blockTypes design - we can create separate components for them
|
||||||
const node = (
|
const node = (
|
||||||
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
|
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
|
||||||
<div className="rounded-xlarge bg-white">
|
<div className="rounded-xlarge bg-white">
|
||||||
<NodeHeader data={data} nodeId={nodeId} />
|
<NodeHeader data={data} nodeId={nodeId} />
|
||||||
{isAgent && <SubAgentUpdateFeature nodeID={nodeId} nodeData={data} />}
|
|
||||||
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
|
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
|
||||||
{isAyrshare && <AyrshareConnectButton />}
|
{isAyrshare && <AyrshareConnectButton />}
|
||||||
<FormCreator
|
<FormCreator
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { ArrowUpIcon, WarningIcon } from "@phosphor-icons/react";
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
|
||||||
import { cn, beautifyString } from "@/lib/utils";
|
|
||||||
import { CustomNodeData } from "../../CustomNode";
|
|
||||||
import { useSubAgentUpdateState } from "./useSubAgentUpdateState";
|
|
||||||
import { IncompatibleUpdateDialog } from "./components/IncompatibleUpdateDialog";
|
|
||||||
import { ResolutionModeBar } from "./components/ResolutionModeBar";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inline component for the update bar that can be placed after the header.
|
|
||||||
* Use this inside the node content where you want the bar to appear.
|
|
||||||
*/
|
|
||||||
type SubAgentUpdateFeatureProps = {
|
|
||||||
nodeID: string;
|
|
||||||
nodeData: CustomNodeData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SubAgentUpdateFeature({
|
|
||||||
nodeID,
|
|
||||||
nodeData,
|
|
||||||
}: SubAgentUpdateFeatureProps) {
|
|
||||||
const {
|
|
||||||
updateInfo,
|
|
||||||
isInResolutionMode,
|
|
||||||
handleUpdateClick,
|
|
||||||
showIncompatibilityDialog,
|
|
||||||
setShowIncompatibilityDialog,
|
|
||||||
handleConfirmIncompatibleUpdate,
|
|
||||||
} = useSubAgentUpdateState({ nodeID: nodeID, nodeData: nodeData });
|
|
||||||
|
|
||||||
const agentName = nodeData.title || "Agent";
|
|
||||||
|
|
||||||
if (!updateInfo.hasUpdate && !isInResolutionMode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isInResolutionMode ? (
|
|
||||||
<ResolutionModeBar incompatibilities={updateInfo.incompatibilities} />
|
|
||||||
) : (
|
|
||||||
<SubAgentUpdateAvailableBar
|
|
||||||
currentVersion={updateInfo.currentVersion}
|
|
||||||
latestVersion={updateInfo.latestVersion}
|
|
||||||
isCompatible={updateInfo.isCompatible}
|
|
||||||
onUpdate={handleUpdateClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* Incompatibility dialog - rendered here since this component owns the state */}
|
|
||||||
{updateInfo.incompatibilities && (
|
|
||||||
<IncompatibleUpdateDialog
|
|
||||||
isOpen={showIncompatibilityDialog}
|
|
||||||
onClose={() => setShowIncompatibilityDialog(false)}
|
|
||||||
onConfirm={handleConfirmIncompatibleUpdate}
|
|
||||||
currentVersion={updateInfo.currentVersion}
|
|
||||||
latestVersion={updateInfo.latestVersion}
|
|
||||||
agentName={beautifyString(agentName)}
|
|
||||||
incompatibilities={updateInfo.incompatibilities}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubAgentUpdateAvailableBarProps = {
|
|
||||||
currentVersion: number;
|
|
||||||
latestVersion: number;
|
|
||||||
isCompatible: boolean;
|
|
||||||
onUpdate: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SubAgentUpdateAvailableBar({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
isCompatible,
|
|
||||||
onUpdate,
|
|
||||||
}: SubAgentUpdateAvailableBarProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2 rounded-t-xl bg-blue-50 px-3 py-2 dark:bg-blue-900/30">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ArrowUpIcon className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<span className="text-sm text-blue-700 dark:text-blue-300">
|
|
||||||
Update available (v{currentVersion} → v{latestVersion})
|
|
||||||
</span>
|
|
||||||
{!isCompatible && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<WarningIcon className="h-4 w-4 text-amber-500" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
<p className="font-medium">Incompatible changes detected</p>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Click Update to see details
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant={isCompatible ? "primary" : "outline"}
|
|
||||||
onClick={onUpdate}
|
|
||||||
className={cn(
|
|
||||||
"h-7 text-xs",
|
|
||||||
!isCompatible && "border-amber-500 text-amber-600 hover:bg-amber-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
WarningIcon,
|
|
||||||
XCircleIcon,
|
|
||||||
PlusCircleIcon,
|
|
||||||
} from "@phosphor-icons/react";
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
|
||||||
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
|
|
||||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
|
||||||
import { beautifyString } from "@/lib/utils";
|
|
||||||
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate/types";
|
|
||||||
|
|
||||||
type IncompatibleUpdateDialogProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
currentVersion: number;
|
|
||||||
latestVersion: number;
|
|
||||||
agentName: string;
|
|
||||||
incompatibilities: IncompatibilityInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function IncompatibleUpdateDialog({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onConfirm,
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
agentName,
|
|
||||||
incompatibilities,
|
|
||||||
}: IncompatibleUpdateDialogProps) {
|
|
||||||
const hasMissingInputs = incompatibilities.missingInputs.length > 0;
|
|
||||||
const hasMissingOutputs = incompatibilities.missingOutputs.length > 0;
|
|
||||||
const hasNewInputs = incompatibilities.newInputs.length > 0;
|
|
||||||
const hasNewOutputs = incompatibilities.newOutputs.length > 0;
|
|
||||||
const hasNewRequired = incompatibilities.newRequiredInputs.length > 0;
|
|
||||||
const hasTypeMismatches = incompatibilities.inputTypeMismatches.length > 0;
|
|
||||||
|
|
||||||
const hasInputChanges = hasMissingInputs || hasNewInputs;
|
|
||||||
const hasOutputChanges = hasMissingOutputs || hasNewOutputs;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<WarningIcon className="h-5 w-5 text-amber-500" weight="fill" />
|
|
||||||
Incompatible Update
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
controlled={{
|
|
||||||
isOpen,
|
|
||||||
set: async (open) => {
|
|
||||||
if (!open) onClose();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClose={onClose}
|
|
||||||
styling={{ maxWidth: "32rem" }}
|
|
||||||
>
|
|
||||||
<Dialog.Content>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
Updating <strong>{beautifyString(agentName)}</strong> from v
|
|
||||||
{currentVersion} to v{latestVersion} will break some connections.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Input changes - two column layout */}
|
|
||||||
{hasInputChanges && (
|
|
||||||
<TwoColumnSection
|
|
||||||
title="Input Changes"
|
|
||||||
leftIcon={
|
|
||||||
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
|
|
||||||
}
|
|
||||||
leftTitle="Removed"
|
|
||||||
leftItems={incompatibilities.missingInputs}
|
|
||||||
rightIcon={
|
|
||||||
<PlusCircleIcon
|
|
||||||
className="h-4 w-4 text-green-500"
|
|
||||||
weight="fill"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightTitle="Added"
|
|
||||||
rightItems={incompatibilities.newInputs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Output changes - two column layout */}
|
|
||||||
{hasOutputChanges && (
|
|
||||||
<TwoColumnSection
|
|
||||||
title="Output Changes"
|
|
||||||
leftIcon={
|
|
||||||
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
|
|
||||||
}
|
|
||||||
leftTitle="Removed"
|
|
||||||
leftItems={incompatibilities.missingOutputs}
|
|
||||||
rightIcon={
|
|
||||||
<PlusCircleIcon
|
|
||||||
className="h-4 w-4 text-green-500"
|
|
||||||
weight="fill"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightTitle="Added"
|
|
||||||
rightItems={incompatibilities.newOutputs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasTypeMismatches && (
|
|
||||||
<SingleColumnSection
|
|
||||||
icon={
|
|
||||||
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
|
|
||||||
}
|
|
||||||
title="Type Changed"
|
|
||||||
description="These connected inputs have a different type:"
|
|
||||||
items={incompatibilities.inputTypeMismatches.map(
|
|
||||||
(m) => `${m.name} (${m.oldType} → ${m.newType})`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasNewRequired && (
|
|
||||||
<SingleColumnSection
|
|
||||||
icon={
|
|
||||||
<PlusCircleIcon
|
|
||||||
className="h-4 w-4 text-amber-500"
|
|
||||||
weight="fill"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="New Required Inputs"
|
|
||||||
description="These inputs are now required:"
|
|
||||||
items={incompatibilities.newRequiredInputs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Alert variant="warning">
|
|
||||||
<AlertDescription>
|
|
||||||
If you proceed, you'll need to remove the broken connections
|
|
||||||
before you can save or run your agent.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button variant="ghost" size="small" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={onConfirm}
|
|
||||||
className="border-amber-700 bg-amber-600 hover:bg-amber-700"
|
|
||||||
>
|
|
||||||
Update Anyway
|
|
||||||
</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type TwoColumnSectionProps = {
|
|
||||||
title: string;
|
|
||||||
leftIcon: React.ReactNode;
|
|
||||||
leftTitle: string;
|
|
||||||
leftItems: string[];
|
|
||||||
rightIcon: React.ReactNode;
|
|
||||||
rightTitle: string;
|
|
||||||
rightItems: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function TwoColumnSection({
|
|
||||||
title,
|
|
||||||
leftIcon,
|
|
||||||
leftTitle,
|
|
||||||
leftItems,
|
|
||||||
rightIcon,
|
|
||||||
rightTitle,
|
|
||||||
rightItems,
|
|
||||||
}: TwoColumnSectionProps) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
|
||||||
<span className="font-medium">{title}</span>
|
|
||||||
<div className="mt-2 grid grid-cols-2 items-start gap-4">
|
|
||||||
{/* Left column - Breaking changes */}
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{leftIcon}
|
|
||||||
<span>{leftTitle}</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mt-1.5 space-y-1">
|
|
||||||
{leftItems.length > 0 ? (
|
|
||||||
leftItems.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item}
|
|
||||||
className="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<code className="rounded bg-red-50 px-1 py-0.5 font-mono text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
|
|
||||||
{item}
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
|
||||||
None
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right column - Possible solutions */}
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{rightIcon}
|
|
||||||
<span>{rightTitle}</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mt-1.5 space-y-1">
|
|
||||||
{rightItems.length > 0 ? (
|
|
||||||
rightItems.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item}
|
|
||||||
className="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<code className="rounded bg-green-50 px-1 py-0.5 font-mono text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
|
||||||
{item}
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
|
||||||
None
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleColumnSectionProps = {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
items: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function SingleColumnSection({
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
items,
|
|
||||||
}: SingleColumnSectionProps) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{icon}
|
|
||||||
<span className="font-medium">{title}</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 space-y-1">
|
|
||||||
{items.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item}
|
|
||||||
className="ml-4 list-disc text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs dark:bg-gray-800">
|
|
||||||
{item}
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { InfoIcon, WarningIcon } from "@phosphor-icons/react";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
|
||||||
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate/types";
|
|
||||||
|
|
||||||
type ResolutionModeBarProps = {
|
|
||||||
incompatibilities: IncompatibilityInfo | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ResolutionModeBar({
|
|
||||||
incompatibilities,
|
|
||||||
}: ResolutionModeBarProps): React.ReactElement {
|
|
||||||
const renderIncompatibilities = () => {
|
|
||||||
if (!incompatibilities) return <span>No incompatibilities</span>;
|
|
||||||
|
|
||||||
const sections: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
if (incompatibilities.missingInputs.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
<div key="missing-inputs" className="mb-1">
|
|
||||||
<span className="font-semibold">Missing inputs: </span>
|
|
||||||
{incompatibilities.missingInputs.map((name, i) => (
|
|
||||||
<React.Fragment key={name}>
|
|
||||||
<code className="font-mono">{name}</code>
|
|
||||||
{i < incompatibilities.missingInputs.length - 1 && ", "}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (incompatibilities.missingOutputs.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
<div key="missing-outputs" className="mb-1">
|
|
||||||
<span className="font-semibold">Missing outputs: </span>
|
|
||||||
{incompatibilities.missingOutputs.map((name, i) => (
|
|
||||||
<React.Fragment key={name}>
|
|
||||||
<code className="font-mono">{name}</code>
|
|
||||||
{i < incompatibilities.missingOutputs.length - 1 && ", "}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (incompatibilities.newRequiredInputs.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
<div key="new-required" className="mb-1">
|
|
||||||
<span className="font-semibold">New required inputs: </span>
|
|
||||||
{incompatibilities.newRequiredInputs.map((name, i) => (
|
|
||||||
<React.Fragment key={name}>
|
|
||||||
<code className="font-mono">{name}</code>
|
|
||||||
{i < incompatibilities.newRequiredInputs.length - 1 && ", "}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (incompatibilities.inputTypeMismatches.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
<div key="type-mismatches" className="mb-1">
|
|
||||||
<span className="font-semibold">Type changed: </span>
|
|
||||||
{incompatibilities.inputTypeMismatches.map((m, i) => (
|
|
||||||
<React.Fragment key={m.name}>
|
|
||||||
<code className="font-mono">{m.name}</code>
|
|
||||||
<span className="text-gray-400">
|
|
||||||
{" "}
|
|
||||||
({m.oldType} → {m.newType})
|
|
||||||
</span>
|
|
||||||
{i < incompatibilities.inputTypeMismatches.length - 1 && ", "}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{sections}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2 rounded-t-xl bg-amber-50 px-3 py-2 dark:bg-amber-900/30">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<WarningIcon className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
|
||||||
<span className="text-sm text-amber-700 dark:text-amber-300">
|
|
||||||
Remove incompatible connections
|
|
||||||
</span>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<InfoIcon className="h-4 w-4 cursor-help text-amber-500" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-sm">
|
|
||||||
<p className="mb-2 font-semibold">Incompatible changes:</p>
|
|
||||||
<div className="text-xs">{renderIncompatibilities()}</div>
|
|
||||||
<p className="mt-2 text-xs text-gray-400">
|
|
||||||
{(incompatibilities?.newRequiredInputs.length ?? 0) > 0
|
|
||||||
? "Replace / delete"
|
|
||||||
: "Delete"}{" "}
|
|
||||||
the red connections to continue
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
|
||||||
import { useShallow } from "zustand/react/shallow";
|
|
||||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
|
||||||
import {
|
|
||||||
useNodeStore,
|
|
||||||
NodeResolutionData,
|
|
||||||
} from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
|
||||||
import {
|
|
||||||
useSubAgentUpdate,
|
|
||||||
createUpdatedAgentNodeInputs,
|
|
||||||
getBrokenEdgeIDs,
|
|
||||||
} from "@/app/(platform)/build/hooks/useSubAgentUpdate";
|
|
||||||
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
|
||||||
import { CustomNodeData } from "../../CustomNode";
|
|
||||||
|
|
||||||
// Stable empty set to avoid creating new references in selectors
|
|
||||||
const EMPTY_SET: Set<string> = new Set();
|
|
||||||
|
|
||||||
type UseSubAgentUpdateParams = {
|
|
||||||
nodeID: string;
|
|
||||||
nodeData: CustomNodeData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useSubAgentUpdateState({
|
|
||||||
nodeID,
|
|
||||||
nodeData,
|
|
||||||
}: UseSubAgentUpdateParams) {
|
|
||||||
const [showIncompatibilityDialog, setShowIncompatibilityDialog] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
// Get store actions
|
|
||||||
const updateNodeData = useNodeStore(
|
|
||||||
useShallow((state) => state.updateNodeData),
|
|
||||||
);
|
|
||||||
const setNodeResolutionMode = useNodeStore(
|
|
||||||
useShallow((state) => state.setNodeResolutionMode),
|
|
||||||
);
|
|
||||||
const isNodeInResolutionMode = useNodeStore(
|
|
||||||
useShallow((state) => state.isNodeInResolutionMode),
|
|
||||||
);
|
|
||||||
const setBrokenEdgeIDs = useNodeStore(
|
|
||||||
useShallow((state) => state.setBrokenEdgeIDs),
|
|
||||||
);
|
|
||||||
// Get this node's broken edge IDs from the per-node map
|
|
||||||
// Use EMPTY_SET as fallback to maintain referential stability
|
|
||||||
const brokenEdgeIDs = useNodeStore(
|
|
||||||
(state) => state.brokenEdgeIDs.get(nodeID) || EMPTY_SET,
|
|
||||||
);
|
|
||||||
const getNodeResolutionData = useNodeStore(
|
|
||||||
useShallow((state) => state.getNodeResolutionData),
|
|
||||||
);
|
|
||||||
const connectedEdges = useEdgeStore(
|
|
||||||
useShallow((state) => state.getNodeEdges(nodeID)),
|
|
||||||
);
|
|
||||||
const availableSubGraphs = useGraphStore(
|
|
||||||
useShallow((state) => state.availableSubGraphs),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Extract agent-specific data
|
|
||||||
const graphID = nodeData.hardcodedValues?.graph_id as string | undefined;
|
|
||||||
const graphVersion = nodeData.hardcodedValues?.graph_version as
|
|
||||||
| number
|
|
||||||
| undefined;
|
|
||||||
const currentInputSchema = nodeData.hardcodedValues?.input_schema as
|
|
||||||
| GraphInputSchema
|
|
||||||
| undefined;
|
|
||||||
const currentOutputSchema = nodeData.hardcodedValues?.output_schema as
|
|
||||||
| GraphOutputSchema
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
// Use the sub-agent update hook
|
|
||||||
const updateInfo = useSubAgentUpdate(
|
|
||||||
nodeID,
|
|
||||||
graphID,
|
|
||||||
graphVersion,
|
|
||||||
currentInputSchema,
|
|
||||||
currentOutputSchema,
|
|
||||||
connectedEdges,
|
|
||||||
availableSubGraphs,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isInResolutionMode = isNodeInResolutionMode(nodeID);
|
|
||||||
|
|
||||||
// Handle update button click
|
|
||||||
const handleUpdateClick = useCallback(() => {
|
|
||||||
if (!updateInfo.hasUpdate || !updateInfo.latestGraph) return;
|
|
||||||
|
|
||||||
if (updateInfo.isCompatible) {
|
|
||||||
// Compatible update - apply directly
|
|
||||||
const newHardcodedValues = createUpdatedAgentNodeInputs(
|
|
||||||
nodeData.hardcodedValues,
|
|
||||||
updateInfo.latestGraph,
|
|
||||||
);
|
|
||||||
updateNodeData(nodeID, { hardcodedValues: newHardcodedValues });
|
|
||||||
} else {
|
|
||||||
// Incompatible update - show dialog
|
|
||||||
setShowIncompatibilityDialog(true);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
updateInfo.hasUpdate,
|
|
||||||
updateInfo.latestGraph,
|
|
||||||
updateInfo.isCompatible,
|
|
||||||
nodeData.hardcodedValues,
|
|
||||||
updateNodeData,
|
|
||||||
nodeID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Handle confirming an incompatible update
|
|
||||||
function handleConfirmIncompatibleUpdate() {
|
|
||||||
if (!updateInfo.latestGraph || !updateInfo.incompatibilities) return;
|
|
||||||
|
|
||||||
const latestGraph = updateInfo.latestGraph;
|
|
||||||
|
|
||||||
// Get the new schemas from the latest graph version
|
|
||||||
const newInputSchema =
|
|
||||||
(latestGraph.input_schema as Record<string, unknown>) || {};
|
|
||||||
const newOutputSchema =
|
|
||||||
(latestGraph.output_schema as Record<string, unknown>) || {};
|
|
||||||
|
|
||||||
// Create the updated hardcoded values but DON'T apply them yet
|
|
||||||
// We'll apply them when resolution is complete
|
|
||||||
const pendingHardcodedValues = createUpdatedAgentNodeInputs(
|
|
||||||
nodeData.hardcodedValues,
|
|
||||||
latestGraph,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get broken edge IDs and store them for this node
|
|
||||||
const brokenIds = getBrokenEdgeIDs(
|
|
||||||
connectedEdges,
|
|
||||||
updateInfo.incompatibilities,
|
|
||||||
nodeID,
|
|
||||||
);
|
|
||||||
setBrokenEdgeIDs(nodeID, brokenIds);
|
|
||||||
|
|
||||||
// Enter resolution mode with both old and new schemas
|
|
||||||
// DON'T apply the update yet - keep old schema so connections remain visible
|
|
||||||
const resolutionData: NodeResolutionData = {
|
|
||||||
incompatibilities: updateInfo.incompatibilities,
|
|
||||||
pendingUpdate: {
|
|
||||||
input_schema: newInputSchema,
|
|
||||||
output_schema: newOutputSchema,
|
|
||||||
},
|
|
||||||
currentSchema: {
|
|
||||||
input_schema: (currentInputSchema as Record<string, unknown>) || {},
|
|
||||||
output_schema: (currentOutputSchema as Record<string, unknown>) || {},
|
|
||||||
},
|
|
||||||
pendingHardcodedValues,
|
|
||||||
};
|
|
||||||
setNodeResolutionMode(nodeID, true, resolutionData);
|
|
||||||
|
|
||||||
setShowIncompatibilityDialog(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resolution is complete (all broken edges removed)
|
|
||||||
const resolutionData = getNodeResolutionData(nodeID);
|
|
||||||
|
|
||||||
// Auto-check resolution on edge changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isInResolutionMode) return;
|
|
||||||
|
|
||||||
// Check if any broken edges still exist
|
|
||||||
const remainingBroken = Array.from(brokenEdgeIDs).filter((edgeId) =>
|
|
||||||
connectedEdges.some((e) => e.id === edgeId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remainingBroken.length === 0) {
|
|
||||||
// Resolution complete - now apply the pending update
|
|
||||||
if (resolutionData?.pendingHardcodedValues) {
|
|
||||||
updateNodeData(nodeID, {
|
|
||||||
hardcodedValues: resolutionData.pendingHardcodedValues,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// setNodeResolutionMode will clean up this node's broken edges automatically
|
|
||||||
setNodeResolutionMode(nodeID, false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isInResolutionMode,
|
|
||||||
brokenEdgeIDs,
|
|
||||||
connectedEdges,
|
|
||||||
resolutionData,
|
|
||||||
nodeID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateInfo,
|
|
||||||
isInResolutionMode,
|
|
||||||
resolutionData,
|
|
||||||
showIncompatibilityDialog,
|
|
||||||
setShowIncompatibilityDialog,
|
|
||||||
handleUpdateClick,
|
|
||||||
handleConfirmIncompatibleUpdate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
import { NodeResolutionData } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
import { RJSFSchema } from "@rjsf/utils";
|
|
||||||
|
|
||||||
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
||||||
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
||||||
@@ -11,48 +9,3 @@ export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
|||||||
TERMINATED: "ring-orange-300 bg-orange-300 ",
|
TERMINATED: "ring-orange-300 bg-orange-300 ",
|
||||||
FAILED: "ring-red-300 bg-red-300",
|
FAILED: "ring-red-300 bg-red-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges schemas during resolution mode to include removed inputs/outputs
|
|
||||||
* that still have connections, so users can see and delete them.
|
|
||||||
*/
|
|
||||||
export function mergeSchemaForResolution(
|
|
||||||
currentSchema: Record<string, unknown>,
|
|
||||||
newSchema: Record<string, unknown>,
|
|
||||||
resolutionData: NodeResolutionData,
|
|
||||||
type: "input" | "output",
|
|
||||||
): Record<string, unknown> {
|
|
||||||
const newProps = (newSchema.properties as RJSFSchema) || {};
|
|
||||||
const currentProps = (currentSchema.properties as RJSFSchema) || {};
|
|
||||||
const mergedProps = { ...newProps };
|
|
||||||
const incomp = resolutionData.incompatibilities;
|
|
||||||
|
|
||||||
if (type === "input") {
|
|
||||||
// Add back missing inputs that have connections
|
|
||||||
incomp.missingInputs.forEach((inputName: string) => {
|
|
||||||
if (currentProps[inputName]) {
|
|
||||||
mergedProps[inputName] = currentProps[inputName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Add back inputs with type mismatches (keep old type so connection works visually)
|
|
||||||
incomp.inputTypeMismatches.forEach(
|
|
||||||
(mismatch: { name: string; oldType: string; newType: string }) => {
|
|
||||||
if (currentProps[mismatch.name]) {
|
|
||||||
mergedProps[mismatch.name] = currentProps[mismatch.name];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Add back missing outputs that have connections
|
|
||||||
incomp.missingOutputs.forEach((outputName: string) => {
|
|
||||||
if (currentProps[outputName]) {
|
|
||||||
mergedProps[outputName] = currentProps[outputName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...newSchema,
|
|
||||||
properties: mergedProps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
import { CustomNodeData } from "./CustomNode";
|
|
||||||
import { BlockUIType } from "../../../types";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { mergeSchemaForResolution } from "./helpers";
|
|
||||||
|
|
||||||
export const useCustomNode = ({
|
|
||||||
data,
|
|
||||||
nodeId,
|
|
||||||
}: {
|
|
||||||
data: CustomNodeData;
|
|
||||||
nodeId: string;
|
|
||||||
}) => {
|
|
||||||
const isInResolutionMode = useNodeStore((state) =>
|
|
||||||
state.nodesInResolutionMode.has(nodeId),
|
|
||||||
);
|
|
||||||
const resolutionData = useNodeStore((state) =>
|
|
||||||
state.nodeResolutionData.get(nodeId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isAgent = data.uiType === BlockUIType.AGENT;
|
|
||||||
|
|
||||||
const currentInputSchema = isAgent
|
|
||||||
? (data.hardcodedValues.input_schema ?? {})
|
|
||||||
: data.inputSchema;
|
|
||||||
const currentOutputSchema = isAgent
|
|
||||||
? (data.hardcodedValues.output_schema ?? {})
|
|
||||||
: data.outputSchema;
|
|
||||||
|
|
||||||
const inputSchema = useMemo(() => {
|
|
||||||
if (isAgent && isInResolutionMode && resolutionData) {
|
|
||||||
return mergeSchemaForResolution(
|
|
||||||
resolutionData.currentSchema.input_schema,
|
|
||||||
resolutionData.pendingUpdate.input_schema,
|
|
||||||
resolutionData,
|
|
||||||
"input",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return currentInputSchema;
|
|
||||||
}, [isAgent, isInResolutionMode, resolutionData, currentInputSchema]);
|
|
||||||
|
|
||||||
const outputSchema = useMemo(() => {
|
|
||||||
if (isAgent && isInResolutionMode && resolutionData) {
|
|
||||||
return mergeSchemaForResolution(
|
|
||||||
resolutionData.currentSchema.output_schema,
|
|
||||||
resolutionData.pendingUpdate.output_schema,
|
|
||||||
resolutionData,
|
|
||||||
"output",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return currentOutputSchema;
|
|
||||||
}, [isAgent, isInResolutionMode, resolutionData, currentOutputSchema]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
inputSchema,
|
|
||||||
outputSchema,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -5,16 +5,20 @@ import { useNodeStore } from "../../../stores/nodeStore";
|
|||||||
import { BlockUIType } from "../../types";
|
import { BlockUIType } from "../../types";
|
||||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||||
|
|
||||||
interface FormCreatorProps {
|
export const FormCreator = React.memo(
|
||||||
jsonSchema: RJSFSchema;
|
({
|
||||||
nodeId: string;
|
jsonSchema,
|
||||||
uiType: BlockUIType;
|
nodeId,
|
||||||
showHandles?: boolean;
|
uiType,
|
||||||
className?: string;
|
showHandles = true,
|
||||||
}
|
className,
|
||||||
|
}: {
|
||||||
export const FormCreator: React.FC<FormCreatorProps> = React.memo(
|
jsonSchema: RJSFSchema;
|
||||||
({ jsonSchema, nodeId, uiType, showHandles = true, className }) => {
|
nodeId: string;
|
||||||
|
uiType: BlockUIType;
|
||||||
|
showHandles?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||||
|
|
||||||
const getHardCodedValues = useNodeStore(
|
const getHardCodedValues = useNodeStore(
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
import { getTypeDisplayInfo } from "./helpers";
|
import { getTypeDisplayInfo } from "./helpers";
|
||||||
import { BlockUIType } from "../../types";
|
import { BlockUIType } from "../../types";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useBrokenOutputs } from "./useBrokenOutputs";
|
|
||||||
|
|
||||||
export const OutputHandler = ({
|
export const OutputHandler = ({
|
||||||
outputSchema,
|
outputSchema,
|
||||||
@@ -29,9 +27,6 @@ export const OutputHandler = ({
|
|||||||
const { isOutputConnected } = useEdgeStore();
|
const { isOutputConnected } = useEdgeStore();
|
||||||
const properties = outputSchema?.properties || {};
|
const properties = outputSchema?.properties || {};
|
||||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||||
const brokenOutputs = useBrokenOutputs(nodeId);
|
|
||||||
|
|
||||||
console.log("brokenOutputs", brokenOutputs);
|
|
||||||
|
|
||||||
const showHandles = uiType !== BlockUIType.OUTPUT;
|
const showHandles = uiType !== BlockUIType.OUTPUT;
|
||||||
|
|
||||||
@@ -49,7 +44,6 @@ export const OutputHandler = ({
|
|||||||
const shouldShow = isConnected || isOutputVisible;
|
const shouldShow = isConnected || isOutputVisible;
|
||||||
const { displayType, colorClass, hexColor } =
|
const { displayType, colorClass, hexColor } =
|
||||||
getTypeDisplayInfo(fieldSchema);
|
getTypeDisplayInfo(fieldSchema);
|
||||||
const isBroken = brokenOutputs.has(fullKey);
|
|
||||||
|
|
||||||
return shouldShow ? (
|
return shouldShow ? (
|
||||||
<div key={fullKey} className="flex flex-col items-end gap-2">
|
<div key={fullKey} className="flex flex-col items-end gap-2">
|
||||||
@@ -70,29 +64,15 @@ export const OutputHandler = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
<Text
|
<Text variant="body" className="text-slate-700">
|
||||||
variant="body"
|
|
||||||
className={cn(
|
|
||||||
"text-slate-700",
|
|
||||||
isBroken && "text-red-500 line-through",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{fieldTitle}
|
{fieldTitle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text variant="small" as="span" className={colorClass}>
|
||||||
variant="small"
|
|
||||||
as="span"
|
|
||||||
className={cn(
|
|
||||||
colorClass,
|
|
||||||
isBroken && "!text-red-500 line-through",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
({displayType})
|
({displayType})
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{showHandles && (
|
{showHandles && (
|
||||||
<OutputNodeHandle
|
<OutputNodeHandle
|
||||||
isBroken={isBroken}
|
|
||||||
field_name={fullKey}
|
field_name={fullKey}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
hexColor={hexColor}
|
hexColor={hexColor}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to get the set of broken output names for a node in resolution mode.
|
|
||||||
*/
|
|
||||||
export function useBrokenOutputs(nodeID: string): Set<string> {
|
|
||||||
// Subscribe to the actual state values, not just methods
|
|
||||||
const isInResolution = useNodeStore((state) =>
|
|
||||||
state.nodesInResolutionMode.has(nodeID),
|
|
||||||
);
|
|
||||||
const resolutionData = useNodeStore((state) =>
|
|
||||||
state.nodeResolutionData.get(nodeID),
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!isInResolution || !resolutionData) {
|
|
||||||
return new Set<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Set(resolutionData.incompatibilities.missingOutputs);
|
|
||||||
}, [isInResolution, resolutionData]);
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ export const RightSidebar = () => {
|
|||||||
>
|
>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-200">
|
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-200">
|
||||||
Graph Debug Panel
|
Flow Debug Panel
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export const RightSidebar = () => {
|
|||||||
{l.source_id}[{l.source_name}] → {l.sink_id}[{l.sink_name}]
|
{l.source_id}[{l.source_name}] → {l.sink_id}[{l.sink_name}]
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-slate-500 dark:text-slate-400">
|
<div className="mt-1 text-slate-500 dark:text-slate-400">
|
||||||
edge.id: {l.id}
|
edge_id: {l.id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -12,14 +12,7 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/__legacy__/ui/popover";
|
} from "@/components/__legacy__/ui/popover";
|
||||||
import {
|
import { Block, BlockUIType, SpecialBlockID } from "@/lib/autogpt-server-api";
|
||||||
Block,
|
|
||||||
BlockIORootSchema,
|
|
||||||
BlockUIType,
|
|
||||||
GraphInputSchema,
|
|
||||||
GraphOutputSchema,
|
|
||||||
SpecialBlockID,
|
|
||||||
} from "@/lib/autogpt-server-api";
|
|
||||||
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||||
import { IconToyBrick } from "@/components/__legacy__/ui/icons";
|
import { IconToyBrick } from "@/components/__legacy__/ui/icons";
|
||||||
import { getPrimaryCategoryColor } from "@/lib/utils";
|
import { getPrimaryCategoryColor } from "@/lib/utils";
|
||||||
@@ -31,10 +24,8 @@ import {
|
|||||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||||
import jaro from "jaro-winkler";
|
import jaro from "jaro-winkler";
|
||||||
|
|
||||||
type _Block = Omit<Block, "inputSchema" | "outputSchema"> & {
|
type _Block = Block & {
|
||||||
uiKey?: string;
|
uiKey?: string;
|
||||||
inputSchema: BlockIORootSchema | GraphInputSchema;
|
|
||||||
outputSchema: BlockIORootSchema | GraphOutputSchema;
|
|
||||||
hardcodedValues?: Record<string, any>;
|
hardcodedValues?: Record<string, any>;
|
||||||
_cached?: {
|
_cached?: {
|
||||||
blockName: string;
|
blockName: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/__legacy__/ui/button";
|
import { Button } from "@/components/__legacy__/ui/button";
|
||||||
import { LogOut } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
import { ClockIcon, WarningIcon } from "@phosphor-icons/react";
|
import { ClockIcon } from "@phosphor-icons/react";
|
||||||
import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons";
|
import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -13,7 +13,6 @@ interface Props {
|
|||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
resolutionModeActive?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BuildActionBar: React.FC<Props> = ({
|
export const BuildActionBar: React.FC<Props> = ({
|
||||||
@@ -24,30 +23,9 @@ export const BuildActionBar: React.FC<Props> = ({
|
|||||||
isRunning,
|
isRunning,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
className,
|
className,
|
||||||
resolutionModeActive = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
const buttonClasses =
|
const buttonClasses =
|
||||||
"flex items-center gap-2 text-sm font-medium md:text-lg";
|
"flex items-center gap-2 text-sm font-medium md:text-lg";
|
||||||
|
|
||||||
// Show resolution mode message instead of action buttons
|
|
||||||
if (resolutionModeActive) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit select-none items-center justify-center p-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-700 dark:bg-amber-900/30">
|
|
||||||
<WarningIcon className="size-5 text-amber-600 dark:text-amber-400" />
|
|
||||||
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
|
||||||
Remove incompatible connections to continue
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -60,16 +60,10 @@ export function CustomEdge({
|
|||||||
targetY - 5,
|
targetY - 5,
|
||||||
);
|
);
|
||||||
const { deleteElements } = useReactFlow<Node, CustomEdge>();
|
const { deleteElements } = useReactFlow<Node, CustomEdge>();
|
||||||
const builderContext = useContext(BuilderContext);
|
const { visualizeBeads } = useContext(BuilderContext) ?? {
|
||||||
const { visualizeBeads } = builderContext ?? {
|
|
||||||
visualizeBeads: "no",
|
visualizeBeads: "no",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if this edge is broken (during resolution mode)
|
|
||||||
const isBroken =
|
|
||||||
builderContext?.resolutionMode?.active &&
|
|
||||||
builderContext?.resolutionMode?.brokenEdgeIds?.includes(id);
|
|
||||||
|
|
||||||
const onEdgeRemoveClick = () => {
|
const onEdgeRemoveClick = () => {
|
||||||
deleteElements({ edges: [{ id }] });
|
deleteElements({ edges: [{ id }] });
|
||||||
};
|
};
|
||||||
@@ -177,27 +171,12 @@ export function CustomEdge({
|
|||||||
|
|
||||||
const middle = getPointForT(0.5);
|
const middle = getPointForT(0.5);
|
||||||
|
|
||||||
// Determine edge color - red for broken edges
|
|
||||||
const baseColor = data?.edgeColor ?? "#555555";
|
|
||||||
const edgeColor = isBroken ? "#ef4444" : baseColor;
|
|
||||||
// Add opacity to hex color (99 = 60% opacity, 80 = 50% opacity)
|
|
||||||
const strokeColor = isBroken
|
|
||||||
? `${edgeColor}99`
|
|
||||||
: selected
|
|
||||||
? edgeColor
|
|
||||||
: `${edgeColor}80`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
path={svgPath}
|
path={svgPath}
|
||||||
markerEnd={markerEnd}
|
markerEnd={markerEnd}
|
||||||
style={{
|
className={`data-sentry-unmask transition-all duration-200 ${data?.isStatic ? "[stroke-dasharray:5_3]" : "[stroke-dasharray:0]"} [stroke-width:${data?.isStatic ? 2.5 : 2}px] hover:[stroke-width:${data?.isStatic ? 3.5 : 3}px] ${selected ? `[stroke:${data?.edgeColor ?? "#555555"}]` : `[stroke:${data?.edgeColor ?? "#555555"}80] hover:[stroke:${data?.edgeColor ?? "#555555"}]`}`}
|
||||||
stroke: strokeColor,
|
|
||||||
strokeWidth: data?.isStatic ? 2.5 : 2,
|
|
||||||
strokeDasharray: data?.isStatic ? "5 3" : undefined,
|
|
||||||
}}
|
|
||||||
className="data-sentry-unmask transition-all duration-200"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={svgPath}
|
d={svgPath}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import {
|
|||||||
BlockIOSubSchema,
|
BlockIOSubSchema,
|
||||||
BlockUIType,
|
BlockUIType,
|
||||||
Category,
|
Category,
|
||||||
GraphInputSchema,
|
|
||||||
GraphOutputSchema,
|
|
||||||
NodeExecutionResult,
|
NodeExecutionResult,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
import {
|
import {
|
||||||
@@ -64,21 +62,14 @@ import { NodeGenericInputField, NodeTextBoxInput } from "../NodeInputs";
|
|||||||
import NodeOutputs from "../NodeOutputs";
|
import NodeOutputs from "../NodeOutputs";
|
||||||
import OutputModalComponent from "../OutputModalComponent";
|
import OutputModalComponent from "../OutputModalComponent";
|
||||||
import "./customnode.css";
|
import "./customnode.css";
|
||||||
import { SubAgentUpdateBar } from "./SubAgentUpdateBar";
|
|
||||||
import { IncompatibilityDialog } from "./IncompatibilityDialog";
|
|
||||||
import {
|
|
||||||
useSubAgentUpdate,
|
|
||||||
createUpdatedAgentNodeInputs,
|
|
||||||
getBrokenEdgeIDs,
|
|
||||||
} from "../../../hooks/useSubAgentUpdate";
|
|
||||||
|
|
||||||
export type ConnectedEdge = {
|
export type ConnectionData = Array<{
|
||||||
id: string;
|
edge_id: string;
|
||||||
source: string;
|
source: string;
|
||||||
sourceHandle: string;
|
sourceHandle: string;
|
||||||
target: string;
|
target: string;
|
||||||
targetHandle: string;
|
targetHandle: string;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export type CustomNodeData = {
|
export type CustomNodeData = {
|
||||||
blockType: string;
|
blockType: string;
|
||||||
@@ -89,7 +80,7 @@ export type CustomNodeData = {
|
|||||||
inputSchema: BlockIORootSchema;
|
inputSchema: BlockIORootSchema;
|
||||||
outputSchema: BlockIORootSchema;
|
outputSchema: BlockIORootSchema;
|
||||||
hardcodedValues: { [key: string]: any };
|
hardcodedValues: { [key: string]: any };
|
||||||
connections: ConnectedEdge[];
|
connections: ConnectionData;
|
||||||
isOutputOpen: boolean;
|
isOutputOpen: boolean;
|
||||||
status?: NodeExecutionResult["status"];
|
status?: NodeExecutionResult["status"];
|
||||||
/** executionResults contains outputs across multiple executions
|
/** executionResults contains outputs across multiple executions
|
||||||
@@ -136,199 +127,20 @@ export const CustomNode = React.memo(
|
|||||||
|
|
||||||
let subGraphID = "";
|
let subGraphID = "";
|
||||||
|
|
||||||
|
if (data.uiType === BlockUIType.AGENT) {
|
||||||
|
// Display the graph's schema instead AgentExecutorBlock's schema.
|
||||||
|
data.inputSchema = data.hardcodedValues?.input_schema || {};
|
||||||
|
data.outputSchema = data.hardcodedValues?.output_schema || {};
|
||||||
|
subGraphID = data.hardcodedValues?.graph_id || subGraphID;
|
||||||
|
}
|
||||||
|
|
||||||
if (!builderContext) {
|
if (!builderContext) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"BuilderContext consumer must be inside FlowEditor component",
|
"BuilderContext consumer must be inside FlowEditor component",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { libraryAgent, setIsAnyModalOpen, getNextNodeId } = builderContext;
|
||||||
libraryAgent,
|
|
||||||
setIsAnyModalOpen,
|
|
||||||
getNextNodeId,
|
|
||||||
availableFlows,
|
|
||||||
resolutionMode,
|
|
||||||
enterResolutionMode,
|
|
||||||
} = builderContext;
|
|
||||||
|
|
||||||
// Check if this node is in resolution mode (moved up for schema merge logic)
|
|
||||||
const isInResolutionMode =
|
|
||||||
resolutionMode.active && resolutionMode.nodeId === id;
|
|
||||||
|
|
||||||
if (data.uiType === BlockUIType.AGENT) {
|
|
||||||
// Display the graph's schema instead AgentExecutorBlock's schema.
|
|
||||||
const currentInputSchema = data.hardcodedValues?.input_schema || {};
|
|
||||||
const currentOutputSchema = data.hardcodedValues?.output_schema || {};
|
|
||||||
subGraphID = data.hardcodedValues?.graph_id || subGraphID;
|
|
||||||
|
|
||||||
// During resolution mode, merge old connected inputs/outputs with new schema
|
|
||||||
if (isInResolutionMode && resolutionMode.pendingUpdate) {
|
|
||||||
const newInputSchema =
|
|
||||||
(resolutionMode.pendingUpdate.input_schema as BlockIORootSchema) ||
|
|
||||||
{};
|
|
||||||
const newOutputSchema =
|
|
||||||
(resolutionMode.pendingUpdate.output_schema as BlockIORootSchema) ||
|
|
||||||
{};
|
|
||||||
|
|
||||||
// Merge input schemas: start with new schema, add old connected inputs that are missing
|
|
||||||
const mergedInputProps = { ...newInputSchema.properties };
|
|
||||||
const incomp = resolutionMode.incompatibilities;
|
|
||||||
if (incomp && currentInputSchema.properties) {
|
|
||||||
// Add back missing inputs that have connections (so user can see/delete them)
|
|
||||||
incomp.missingInputs.forEach((inputName) => {
|
|
||||||
if (currentInputSchema.properties[inputName]) {
|
|
||||||
mergedInputProps[inputName] =
|
|
||||||
currentInputSchema.properties[inputName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Add back inputs with type mismatches (keep old type so connection still works visually)
|
|
||||||
incomp.inputTypeMismatches.forEach((mismatch) => {
|
|
||||||
if (currentInputSchema.properties[mismatch.name]) {
|
|
||||||
mergedInputProps[mismatch.name] =
|
|
||||||
currentInputSchema.properties[mismatch.name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge output schemas: start with new schema, add old connected outputs that are missing
|
|
||||||
const mergedOutputProps = { ...newOutputSchema.properties };
|
|
||||||
if (incomp && currentOutputSchema.properties) {
|
|
||||||
incomp.missingOutputs.forEach((outputName) => {
|
|
||||||
if (currentOutputSchema.properties[outputName]) {
|
|
||||||
mergedOutputProps[outputName] =
|
|
||||||
currentOutputSchema.properties[outputName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
data.inputSchema = {
|
|
||||||
...newInputSchema,
|
|
||||||
properties: mergedInputProps,
|
|
||||||
};
|
|
||||||
data.outputSchema = {
|
|
||||||
...newOutputSchema,
|
|
||||||
properties: mergedOutputProps,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
data.inputSchema = currentInputSchema;
|
|
||||||
data.outputSchema = currentOutputSchema;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setHardcodedValues = useCallback(
|
|
||||||
(values: any) => {
|
|
||||||
updateNodeData(id, { hardcodedValues: values });
|
|
||||||
},
|
|
||||||
[id, updateNodeData],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sub-agent update detection
|
|
||||||
const isAgentBlock = data.uiType === BlockUIType.AGENT;
|
|
||||||
const graphId = isAgentBlock ? data.hardcodedValues?.graph_id : undefined;
|
|
||||||
const graphVersion = isAgentBlock
|
|
||||||
? data.hardcodedValues?.graph_version
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const subAgentUpdate = useSubAgentUpdate(
|
|
||||||
id,
|
|
||||||
graphId,
|
|
||||||
graphVersion,
|
|
||||||
isAgentBlock
|
|
||||||
? (data.hardcodedValues?.input_schema as GraphInputSchema)
|
|
||||||
: undefined,
|
|
||||||
isAgentBlock
|
|
||||||
? (data.hardcodedValues?.output_schema as GraphOutputSchema)
|
|
||||||
: undefined,
|
|
||||||
data.connections,
|
|
||||||
availableFlows,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [showIncompatibilityDialog, setShowIncompatibilityDialog] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
// Helper to check if a handle is broken (for resolution mode)
|
|
||||||
const isInputHandleBroken = useCallback(
|
|
||||||
(handleName: string): boolean => {
|
|
||||||
if (!isInResolutionMode || !resolutionMode.incompatibilities) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const incomp = resolutionMode.incompatibilities;
|
|
||||||
return (
|
|
||||||
incomp.missingInputs.includes(handleName) ||
|
|
||||||
incomp.inputTypeMismatches.some((m) => m.name === handleName)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[isInResolutionMode, resolutionMode.incompatibilities],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isOutputHandleBroken = useCallback(
|
|
||||||
(handleName: string): boolean => {
|
|
||||||
if (!isInResolutionMode || !resolutionMode.incompatibilities) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return resolutionMode.incompatibilities.missingOutputs.includes(
|
|
||||||
handleName,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[isInResolutionMode, resolutionMode.incompatibilities],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle update button click
|
|
||||||
const handleUpdateClick = useCallback(() => {
|
|
||||||
if (!subAgentUpdate.latestGraph) return;
|
|
||||||
|
|
||||||
if (subAgentUpdate.isCompatible) {
|
|
||||||
// Compatible update - directly apply
|
|
||||||
const updatedValues = createUpdatedAgentNodeInputs(
|
|
||||||
data.hardcodedValues,
|
|
||||||
subAgentUpdate.latestGraph,
|
|
||||||
);
|
|
||||||
setHardcodedValues(updatedValues);
|
|
||||||
toast({
|
|
||||||
title: "Agent updated",
|
|
||||||
description: `Updated to version ${subAgentUpdate.latestVersion}`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Incompatible update - show dialog
|
|
||||||
setShowIncompatibilityDialog(true);
|
|
||||||
}
|
|
||||||
}, [subAgentUpdate, data.hardcodedValues, setHardcodedValues]);
|
|
||||||
|
|
||||||
// Handle confirm incompatible update
|
|
||||||
const handleConfirmIncompatibleUpdate = useCallback(() => {
|
|
||||||
if (!subAgentUpdate.latestGraph || !subAgentUpdate.incompatibilities) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the updated values but DON'T apply them yet
|
|
||||||
const updatedValues = createUpdatedAgentNodeInputs(
|
|
||||||
data.hardcodedValues,
|
|
||||||
subAgentUpdate.latestGraph,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get broken edge IDs
|
|
||||||
const brokenEdgeIds = getBrokenEdgeIDs(
|
|
||||||
data.connections,
|
|
||||||
subAgentUpdate.incompatibilities,
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enter resolution mode with pending update (don't apply schema yet)
|
|
||||||
enterResolutionMode(
|
|
||||||
id,
|
|
||||||
subAgentUpdate.incompatibilities,
|
|
||||||
brokenEdgeIds,
|
|
||||||
updatedValues,
|
|
||||||
);
|
|
||||||
|
|
||||||
setShowIncompatibilityDialog(false);
|
|
||||||
}, [
|
|
||||||
subAgentUpdate,
|
|
||||||
data.hardcodedValues,
|
|
||||||
data.connections,
|
|
||||||
id,
|
|
||||||
enterResolutionMode,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.executionResults || data.status) {
|
if (data.executionResults || data.status) {
|
||||||
@@ -344,6 +156,13 @@ export const CustomNode = React.memo(
|
|||||||
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
||||||
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
|
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
|
||||||
|
|
||||||
|
const setHardcodedValues = useCallback(
|
||||||
|
(values: any) => {
|
||||||
|
updateNodeData(id, { hardcodedValues: values });
|
||||||
|
},
|
||||||
|
[id, updateNodeData],
|
||||||
|
);
|
||||||
|
|
||||||
const handleTitleEdit = useCallback(() => {
|
const handleTitleEdit = useCallback(() => {
|
||||||
setIsEditingTitle(true);
|
setIsEditingTitle(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -436,7 +255,6 @@ export const CustomNode = React.memo(
|
|||||||
isConnected={isOutputHandleConnected(propKey)}
|
isConnected={isOutputHandleConnected(propKey)}
|
||||||
schema={fieldSchema}
|
schema={fieldSchema}
|
||||||
side="right"
|
side="right"
|
||||||
isBroken={isOutputHandleBroken(propKey)}
|
|
||||||
/>
|
/>
|
||||||
{"properties" in fieldSchema &&
|
{"properties" in fieldSchema &&
|
||||||
renderHandles(
|
renderHandles(
|
||||||
@@ -567,7 +385,6 @@ export const CustomNode = React.memo(
|
|||||||
isRequired={isRequired}
|
isRequired={isRequired}
|
||||||
schema={propSchema}
|
schema={propSchema}
|
||||||
side="left"
|
side="left"
|
||||||
isBroken={isInputHandleBroken(propKey)}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
propKey !== "credentials" &&
|
propKey !== "credentials" &&
|
||||||
@@ -1056,22 +873,6 @@ export const CustomNode = React.memo(
|
|||||||
<ContextMenuContent />
|
<ContextMenuContent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sub-agent Update Bar - shown below header */}
|
|
||||||
{isAgentBlock && (subAgentUpdate.hasUpdate || isInResolutionMode) && (
|
|
||||||
<SubAgentUpdateBar
|
|
||||||
currentVersion={subAgentUpdate.currentVersion}
|
|
||||||
latestVersion={subAgentUpdate.latestVersion}
|
|
||||||
isCompatible={subAgentUpdate.isCompatible}
|
|
||||||
incompatibilities={
|
|
||||||
isInResolutionMode
|
|
||||||
? resolutionMode.incompatibilities
|
|
||||||
: subAgentUpdate.incompatibilities
|
|
||||||
}
|
|
||||||
onUpdate={handleUpdateClick}
|
|
||||||
isInResolutionMode={isInResolutionMode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="mx-5 my-6 rounded-b-xl">
|
<div className="mx-5 my-6 rounded-b-xl">
|
||||||
{/* Input Handles */}
|
{/* Input Handles */}
|
||||||
@@ -1243,24 +1044,9 @@ export const CustomNode = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ContextMenu.Root>
|
||||||
<ContextMenu.Root>
|
<ContextMenu.Trigger>{nodeContent()}</ContextMenu.Trigger>
|
||||||
<ContextMenu.Trigger>{nodeContent()}</ContextMenu.Trigger>
|
</ContextMenu.Root>
|
||||||
</ContextMenu.Root>
|
|
||||||
|
|
||||||
{/* Incompatibility Dialog for sub-agent updates */}
|
|
||||||
{isAgentBlock && subAgentUpdate.incompatibilities && (
|
|
||||||
<IncompatibilityDialog
|
|
||||||
isOpen={showIncompatibilityDialog}
|
|
||||||
onClose={() => setShowIncompatibilityDialog(false)}
|
|
||||||
onConfirm={handleConfirmIncompatibleUpdate}
|
|
||||||
currentVersion={subAgentUpdate.currentVersion}
|
|
||||||
latestVersion={subAgentUpdate.latestVersion}
|
|
||||||
agentName={data.blockType || "Agent"}
|
|
||||||
incompatibilities={subAgentUpdate.incompatibilities}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prevProps, nextProps) => {
|
(prevProps, nextProps) => {
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/__legacy__/ui/dialog";
|
|
||||||
import { Button } from "@/components/__legacy__/ui/button";
|
|
||||||
import { AlertTriangle, XCircle, PlusCircle } from "lucide-react";
|
|
||||||
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
|
|
||||||
import { beautifyString } from "@/lib/utils";
|
|
||||||
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
|
|
||||||
|
|
||||||
interface IncompatibilityDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
currentVersion: number;
|
|
||||||
latestVersion: number;
|
|
||||||
agentName: string;
|
|
||||||
incompatibilities: IncompatibilityInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IncompatibilityDialog: React.FC<IncompatibilityDialogProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onConfirm,
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
agentName,
|
|
||||||
incompatibilities,
|
|
||||||
}) => {
|
|
||||||
const hasMissingInputs = incompatibilities.missingInputs.length > 0;
|
|
||||||
const hasMissingOutputs = incompatibilities.missingOutputs.length > 0;
|
|
||||||
const hasNewInputs = incompatibilities.newInputs.length > 0;
|
|
||||||
const hasNewOutputs = incompatibilities.newOutputs.length > 0;
|
|
||||||
const hasNewRequired = incompatibilities.newRequiredInputs.length > 0;
|
|
||||||
const hasTypeMismatches = incompatibilities.inputTypeMismatches.length > 0;
|
|
||||||
|
|
||||||
const hasInputChanges = hasMissingInputs || hasNewInputs;
|
|
||||||
const hasOutputChanges = hasMissingOutputs || hasNewOutputs;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
||||||
<DialogContent className="max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
|
||||||
Incompatible Update
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Updating <strong>{beautifyString(agentName)}</strong> from v
|
|
||||||
{currentVersion} to v{latestVersion} will break some connections.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4 py-2">
|
|
||||||
{/* Input changes - two column layout */}
|
|
||||||
{hasInputChanges && (
|
|
||||||
<TwoColumnSection
|
|
||||||
title="Input Changes"
|
|
||||||
leftIcon={<XCircle className="h-4 w-4 text-red-500" />}
|
|
||||||
leftTitle="Removed"
|
|
||||||
leftItems={incompatibilities.missingInputs}
|
|
||||||
rightIcon={<PlusCircle className="h-4 w-4 text-green-500" />}
|
|
||||||
rightTitle="Added"
|
|
||||||
rightItems={incompatibilities.newInputs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Output changes - two column layout */}
|
|
||||||
{hasOutputChanges && (
|
|
||||||
<TwoColumnSection
|
|
||||||
title="Output Changes"
|
|
||||||
leftIcon={<XCircle className="h-4 w-4 text-red-500" />}
|
|
||||||
leftTitle="Removed"
|
|
||||||
leftItems={incompatibilities.missingOutputs}
|
|
||||||
rightIcon={<PlusCircle className="h-4 w-4 text-green-500" />}
|
|
||||||
rightTitle="Added"
|
|
||||||
rightItems={incompatibilities.newOutputs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasTypeMismatches && (
|
|
||||||
<SingleColumnSection
|
|
||||||
icon={<XCircle className="h-4 w-4 text-red-500" />}
|
|
||||||
title="Type Changed"
|
|
||||||
description="These connected inputs have a different type:"
|
|
||||||
items={incompatibilities.inputTypeMismatches.map(
|
|
||||||
(m) => `${m.name} (${m.oldType} → ${m.newType})`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasNewRequired && (
|
|
||||||
<SingleColumnSection
|
|
||||||
icon={<PlusCircle className="h-4 w-4 text-amber-500" />}
|
|
||||||
title="New Required Inputs"
|
|
||||||
description="These inputs are now required:"
|
|
||||||
items={incompatibilities.newRequiredInputs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert variant="warning">
|
|
||||||
<AlertDescription>
|
|
||||||
If you proceed, you'll need to remove the broken connections
|
|
||||||
before you can save or run your agent.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
|
||||||
<Button variant="outline" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={onConfirm}
|
|
||||||
className="bg-amber-600 hover:bg-amber-700"
|
|
||||||
>
|
|
||||||
Update Anyway
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TwoColumnSectionProps {
|
|
||||||
title: string;
|
|
||||||
leftIcon: React.ReactNode;
|
|
||||||
leftTitle: string;
|
|
||||||
leftItems: string[];
|
|
||||||
rightIcon: React.ReactNode;
|
|
||||||
rightTitle: string;
|
|
||||||
rightItems: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const TwoColumnSection: React.FC<TwoColumnSectionProps> = ({
|
|
||||||
title,
|
|
||||||
leftIcon,
|
|
||||||
leftTitle,
|
|
||||||
leftItems,
|
|
||||||
rightIcon,
|
|
||||||
rightTitle,
|
|
||||||
rightItems,
|
|
||||||
}) => (
|
|
||||||
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
|
||||||
<span className="font-medium">{title}</span>
|
|
||||||
<div className="mt-2 grid grid-cols-2 items-start gap-4">
|
|
||||||
{/* Left column - Breaking changes */}
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{leftIcon}
|
|
||||||
<span>{leftTitle}</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mt-1.5 space-y-1">
|
|
||||||
{leftItems.length > 0 ? (
|
|
||||||
leftItems.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item}
|
|
||||||
className="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<code className="rounded bg-red-50 px-1 py-0.5 font-mono text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
|
|
||||||
{item}
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
|
||||||
None
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right column - Possible solutions */}
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{rightIcon}
|
|
||||||
<span>{rightTitle}</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mt-1.5 space-y-1">
|
|
||||||
{rightItems.length > 0 ? (
|
|
||||||
rightItems.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item}
|
|
||||||
className="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<code className="rounded bg-green-50 px-1 py-0.5 font-mono text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
|
||||||
{item}
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className="text-sm italic text-gray-400 dark:text-gray-500">
|
|
||||||
None
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface SingleColumnSectionProps {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
items: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const SingleColumnSection: React.FC<SingleColumnSectionProps> = ({
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
items,
|
|
||||||
}) => (
|
|
||||||
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{icon}
|
|
||||||
<span className="font-medium">{title}</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 space-y-1">
|
|
||||||
{items.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item}
|
|
||||||
className="ml-4 list-disc text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs dark:bg-gray-800">
|
|
||||||
{item}
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default IncompatibilityDialog;
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/__legacy__/ui/button";
|
|
||||||
import { ArrowUp, AlertTriangle, Info } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
|
||||||
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface SubAgentUpdateBarProps {
|
|
||||||
currentVersion: number;
|
|
||||||
latestVersion: number;
|
|
||||||
isCompatible: boolean;
|
|
||||||
incompatibilities: IncompatibilityInfo | null;
|
|
||||||
onUpdate: () => void;
|
|
||||||
isInResolutionMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SubAgentUpdateBar: React.FC<SubAgentUpdateBarProps> = ({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
isCompatible,
|
|
||||||
incompatibilities,
|
|
||||||
onUpdate,
|
|
||||||
isInResolutionMode = false,
|
|
||||||
}) => {
|
|
||||||
if (isInResolutionMode) {
|
|
||||||
return <ResolutionModeBar incompatibilities={incompatibilities} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2 rounded-t-lg bg-blue-50 px-3 py-2 dark:bg-blue-900/30">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ArrowUp className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<span className="text-sm text-blue-700 dark:text-blue-300">
|
|
||||||
Update available (v{currentVersion} → v{latestVersion})
|
|
||||||
</span>
|
|
||||||
{!isCompatible && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
<p className="font-medium">Incompatible changes detected</p>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Click Update to see details
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={isCompatible ? "default" : "outline"}
|
|
||||||
onClick={onUpdate}
|
|
||||||
className={cn(
|
|
||||||
"h-7 text-xs",
|
|
||||||
!isCompatible && "border-amber-500 text-amber-600 hover:bg-amber-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ResolutionModeBarProps {
|
|
||||||
incompatibilities: IncompatibilityInfo | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResolutionModeBar: React.FC<ResolutionModeBarProps> = ({
|
|
||||||
incompatibilities,
|
|
||||||
}) => {
|
|
||||||
const formatIncompatibilities = () => {
|
|
||||||
if (!incompatibilities) return "No incompatibilities";
|
|
||||||
|
|
||||||
const items: string[] = [];
|
|
||||||
|
|
||||||
if (incompatibilities.missingInputs.length > 0) {
|
|
||||||
items.push(
|
|
||||||
`Missing inputs: ${incompatibilities.missingInputs.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (incompatibilities.missingOutputs.length > 0) {
|
|
||||||
items.push(
|
|
||||||
`Missing outputs: ${incompatibilities.missingOutputs.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (incompatibilities.newRequiredInputs.length > 0) {
|
|
||||||
items.push(
|
|
||||||
`New required inputs: ${incompatibilities.newRequiredInputs.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (incompatibilities.inputTypeMismatches.length > 0) {
|
|
||||||
const mismatches = incompatibilities.inputTypeMismatches
|
|
||||||
.map((m) => `${m.name} (${m.oldType} → ${m.newType})`)
|
|
||||||
.join(", ");
|
|
||||||
items.push(`Type changed: ${mismatches}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2 rounded-t-lg bg-amber-50 px-3 py-2 dark:bg-amber-900/30">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
|
||||||
<span className="text-sm text-amber-700 dark:text-amber-300">
|
|
||||||
Remove incompatible connections
|
|
||||||
</span>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="h-4 w-4 cursor-help text-amber-500" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-sm whitespace-pre-line">
|
|
||||||
<p className="font-medium">Incompatible changes:</p>
|
|
||||||
<p className="mt-1 text-xs">{formatIncompatibilities()}</p>
|
|
||||||
<p className="mt-2 text-xs text-gray-400">
|
|
||||||
Delete the red connections to continue
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SubAgentUpdateBar;
|
|
||||||
@@ -26,17 +26,15 @@ import {
|
|||||||
applyNodeChanges,
|
applyNodeChanges,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { ConnectedEdge, CustomNode } from "../CustomNode/CustomNode";
|
import { CustomNode } from "../CustomNode/CustomNode";
|
||||||
import "./flow.css";
|
import "./flow.css";
|
||||||
import {
|
import {
|
||||||
BlockUIType,
|
BlockUIType,
|
||||||
formatEdgeID,
|
formatEdgeID,
|
||||||
GraphExecutionID,
|
GraphExecutionID,
|
||||||
GraphID,
|
GraphID,
|
||||||
GraphMeta,
|
|
||||||
LibraryAgent,
|
LibraryAgent,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
|
|
||||||
import { Key, storage } from "@/services/storage/local-storage";
|
import { Key, storage } from "@/services/storage/local-storage";
|
||||||
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
|
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
|
||||||
import { history } from "../history";
|
import { history } from "../history";
|
||||||
@@ -74,30 +72,12 @@ import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle";
|
|||||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||||
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||||
|
|
||||||
export type ResolutionModeState = {
|
|
||||||
active: boolean;
|
|
||||||
nodeId: string | null;
|
|
||||||
incompatibilities: IncompatibilityInfo | null;
|
|
||||||
brokenEdgeIds: string[];
|
|
||||||
pendingUpdate: Record<string, unknown> | null; // The hardcoded values to apply after resolution
|
|
||||||
};
|
|
||||||
|
|
||||||
type BuilderContextType = {
|
type BuilderContextType = {
|
||||||
libraryAgent: LibraryAgent | null;
|
libraryAgent: LibraryAgent | null;
|
||||||
visualizeBeads: "no" | "static" | "animate";
|
visualizeBeads: "no" | "static" | "animate";
|
||||||
setIsAnyModalOpen: (isOpen: boolean) => void;
|
setIsAnyModalOpen: (isOpen: boolean) => void;
|
||||||
getNextNodeId: () => string;
|
getNextNodeId: () => string;
|
||||||
getNodeTitle: (nodeID: string) => string | null;
|
getNodeTitle: (nodeID: string) => string | null;
|
||||||
availableFlows: GraphMeta[];
|
|
||||||
resolutionMode: ResolutionModeState;
|
|
||||||
enterResolutionMode: (
|
|
||||||
nodeId: string,
|
|
||||||
incompatibilities: IncompatibilityInfo,
|
|
||||||
brokenEdgeIds: string[],
|
|
||||||
pendingUpdate: Record<string, unknown>,
|
|
||||||
) => void;
|
|
||||||
exitResolutionMode: () => void;
|
|
||||||
applyPendingUpdate: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeDimension = {
|
export type NodeDimension = {
|
||||||
@@ -192,92 +172,6 @@ const FlowEditor: React.FC<{
|
|||||||
// It stores the dimension of all nodes with position as well
|
// It stores the dimension of all nodes with position as well
|
||||||
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});
|
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});
|
||||||
|
|
||||||
// Resolution mode state for sub-agent incompatible updates
|
|
||||||
const [resolutionMode, setResolutionMode] = useState<ResolutionModeState>({
|
|
||||||
active: false,
|
|
||||||
nodeId: null,
|
|
||||||
incompatibilities: null,
|
|
||||||
brokenEdgeIds: [],
|
|
||||||
pendingUpdate: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const enterResolutionMode = useCallback(
|
|
||||||
(
|
|
||||||
nodeId: string,
|
|
||||||
incompatibilities: IncompatibilityInfo,
|
|
||||||
brokenEdgeIds: string[],
|
|
||||||
pendingUpdate: Record<string, unknown>,
|
|
||||||
) => {
|
|
||||||
setResolutionMode({
|
|
||||||
active: true,
|
|
||||||
nodeId,
|
|
||||||
incompatibilities,
|
|
||||||
brokenEdgeIds,
|
|
||||||
pendingUpdate,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitResolutionMode = useCallback(() => {
|
|
||||||
setResolutionMode({
|
|
||||||
active: false,
|
|
||||||
nodeId: null,
|
|
||||||
incompatibilities: null,
|
|
||||||
brokenEdgeIds: [],
|
|
||||||
pendingUpdate: null,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Apply pending update after resolution mode completes
|
|
||||||
const applyPendingUpdate = useCallback(() => {
|
|
||||||
if (!resolutionMode.nodeId || !resolutionMode.pendingUpdate) return;
|
|
||||||
|
|
||||||
const node = nodes.find((n) => n.id === resolutionMode.nodeId);
|
|
||||||
if (node) {
|
|
||||||
const pendingUpdate = resolutionMode.pendingUpdate as {
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
setNodes((nds) =>
|
|
||||||
nds.map((n) =>
|
|
||||||
n.id === resolutionMode.nodeId
|
|
||||||
? { ...n, data: { ...n.data, hardcodedValues: pendingUpdate } }
|
|
||||||
: n,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
exitResolutionMode();
|
|
||||||
toast({
|
|
||||||
title: "Update complete",
|
|
||||||
description: "Agent has been updated to the new version.",
|
|
||||||
});
|
|
||||||
}, [resolutionMode, nodes, setNodes, exitResolutionMode, toast]);
|
|
||||||
|
|
||||||
// Check if all broken edges have been removed and auto-apply pending update
|
|
||||||
useEffect(() => {
|
|
||||||
if (!resolutionMode.active || resolutionMode.brokenEdgeIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentEdgeIds = new Set(edges.map((e) => e.id));
|
|
||||||
const remainingBrokenEdges = resolutionMode.brokenEdgeIds.filter((id) =>
|
|
||||||
currentEdgeIds.has(id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remainingBrokenEdges.length === 0) {
|
|
||||||
// All broken edges have been removed, apply pending update
|
|
||||||
applyPendingUpdate();
|
|
||||||
} else if (
|
|
||||||
remainingBrokenEdges.length !== resolutionMode.brokenEdgeIds.length
|
|
||||||
) {
|
|
||||||
// Update the list of broken edges
|
|
||||||
setResolutionMode((prev) => ({
|
|
||||||
...prev,
|
|
||||||
brokenEdgeIds: remainingBrokenEdges,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [edges, resolutionMode, applyPendingUpdate]);
|
|
||||||
|
|
||||||
// Set page title with or without graph name
|
// Set page title with or without graph name
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = savedAgent
|
document.title = savedAgent
|
||||||
@@ -537,19 +431,17 @@ const FlowEditor: React.FC<{
|
|||||||
...node.data.connections.filter(
|
...node.data.connections.filter(
|
||||||
(conn) =>
|
(conn) =>
|
||||||
!removedEdges.some(
|
!removedEdges.some(
|
||||||
(removedEdge) => removedEdge.id === conn.id,
|
(removedEdge) => removedEdge.id === conn.edge_id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Add node connections for added edges
|
// Add node connections for added edges
|
||||||
...addedEdges.map(
|
...addedEdges.map((addedEdge) => ({
|
||||||
(addedEdge): ConnectedEdge => ({
|
edge_id: addedEdge.item.id,
|
||||||
id: addedEdge.item.id,
|
source: addedEdge.item.source,
|
||||||
source: addedEdge.item.source,
|
target: addedEdge.item.target,
|
||||||
target: addedEdge.item.target,
|
sourceHandle: addedEdge.item.sourceHandle!,
|
||||||
sourceHandle: addedEdge.item.sourceHandle!,
|
targetHandle: addedEdge.item.targetHandle!,
|
||||||
targetHandle: addedEdge.item.targetHandle!,
|
})),
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -575,15 +467,13 @@ const FlowEditor: React.FC<{
|
|||||||
data: {
|
data: {
|
||||||
...node.data,
|
...node.data,
|
||||||
connections: [
|
connections: [
|
||||||
...replaceEdges.map(
|
...replaceEdges.map((replaceEdge) => ({
|
||||||
(replaceEdge): ConnectedEdge => ({
|
edge_id: replaceEdge.item.id,
|
||||||
id: replaceEdge.item.id,
|
source: replaceEdge.item.source,
|
||||||
source: replaceEdge.item.source,
|
target: replaceEdge.item.target,
|
||||||
target: replaceEdge.item.target,
|
sourceHandle: replaceEdge.item.sourceHandle!,
|
||||||
sourceHandle: replaceEdge.item.sourceHandle!,
|
targetHandle: replaceEdge.item.targetHandle!,
|
||||||
targetHandle: replaceEdge.item.targetHandle!,
|
})),
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
@@ -1000,23 +890,8 @@ const FlowEditor: React.FC<{
|
|||||||
setIsAnyModalOpen,
|
setIsAnyModalOpen,
|
||||||
getNextNodeId,
|
getNextNodeId,
|
||||||
getNodeTitle,
|
getNodeTitle,
|
||||||
availableFlows,
|
|
||||||
resolutionMode,
|
|
||||||
enterResolutionMode,
|
|
||||||
exitResolutionMode,
|
|
||||||
applyPendingUpdate,
|
|
||||||
}),
|
}),
|
||||||
[
|
[libraryAgent, visualizeBeads, getNextNodeId, getNodeTitle],
|
||||||
libraryAgent,
|
|
||||||
visualizeBeads,
|
|
||||||
getNextNodeId,
|
|
||||||
getNodeTitle,
|
|
||||||
availableFlows,
|
|
||||||
resolutionMode,
|
|
||||||
enterResolutionMode,
|
|
||||||
applyPendingUpdate,
|
|
||||||
exitResolutionMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1116,7 +991,6 @@ const FlowEditor: React.FC<{
|
|||||||
onClickScheduleButton={handleScheduleButton}
|
onClickScheduleButton={handleScheduleButton}
|
||||||
isDisabled={!savedAgent}
|
isDisabled={!savedAgent}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
resolutionModeActive={resolutionMode.active}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">
|
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
|
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
|
||||||
import {
|
import { cn } from "@/lib/utils";
|
||||||
cn,
|
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
|
||||||
beautifyString,
|
|
||||||
getTypeBgColor,
|
|
||||||
getTypeTextColor,
|
|
||||||
getEffectiveType,
|
|
||||||
} from "@/lib/utils";
|
|
||||||
import { FC, memo, useCallback } from "react";
|
import { FC, memo, useCallback } from "react";
|
||||||
import { Handle, Position } from "@xyflow/react";
|
import { Handle, Position } from "@xyflow/react";
|
||||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||||
@@ -18,7 +13,6 @@ type HandleProps = {
|
|||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
isBroken?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move the constant out of the component to avoid re-creation on every render.
|
// Move the constant out of the component to avoid re-creation on every render.
|
||||||
@@ -33,23 +27,18 @@ const TYPE_NAME: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
|
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
|
||||||
const Dot: FC<{ isConnected: boolean; type?: string; isBroken?: boolean }> =
|
const Dot: FC<{ isConnected: boolean; type?: string }> = memo(
|
||||||
memo(({ isConnected, type, isBroken }) => {
|
({ isConnected, type }) => {
|
||||||
const color = isBroken
|
const color = isConnected
|
||||||
? "border-red-500 bg-red-100 dark:bg-red-900/30"
|
? getTypeBgColor(type || "any")
|
||||||
: isConnected
|
: "border-gray-300 dark:border-gray-600";
|
||||||
? getTypeBgColor(type || "any")
|
|
||||||
: "border-gray-300 dark:border-gray-600";
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
|
||||||
"m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700",
|
|
||||||
color,
|
|
||||||
isBroken && "opacity-50",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
Dot.displayName = "Dot";
|
Dot.displayName = "Dot";
|
||||||
|
|
||||||
const NodeHandle: FC<HandleProps> = ({
|
const NodeHandle: FC<HandleProps> = ({
|
||||||
@@ -60,34 +49,24 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
side,
|
side,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
isBroken = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
// Extract effective type from schema (handles anyOf/oneOf/allOf wrappers)
|
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${
|
||||||
const effectiveType = getEffectiveType(schema);
|
|
||||||
|
|
||||||
const typeClass = `text-sm ${getTypeTextColor(effectiveType || "any")} ${
|
|
||||||
side === "left" ? "text-left" : "text-right"
|
side === "left" ? "text-left" : "text-right"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const label = (
|
const label = (
|
||||||
<div className={cn("flex flex-grow flex-row", isBroken && "opacity-50")}>
|
<div className="flex flex-grow flex-row">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-sentry-unmask text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100",
|
"data-sentry-unmask text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100",
|
||||||
className,
|
className,
|
||||||
isBroken && "text-red-500 line-through",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title || schema.title || beautifyString(keyName.toLowerCase())}
|
{title || schema.title || beautifyString(keyName.toLowerCase())}
|
||||||
{isRequired ? "*" : ""}
|
{isRequired ? "*" : ""}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className={`${typeClass} data-sentry-unmask flex items-end`}>
|
||||||
className={cn(
|
({TYPE_NAME[schema.type as keyof typeof TYPE_NAME] || "any"})
|
||||||
`${typeClass} data-sentry-unmask flex items-end`,
|
|
||||||
isBroken && "text-red-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
({TYPE_NAME[effectiveType as keyof typeof TYPE_NAME] || "any"})
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -105,7 +84,7 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={keyName}
|
key={keyName}
|
||||||
className={cn("handle-container", isBroken && "pointer-events-none")}
|
className="handle-container"
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -113,15 +92,10 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
data-testid={`input-handle-${keyName}`}
|
data-testid={`input-handle-${keyName}`}
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id={keyName}
|
id={keyName}
|
||||||
className={cn("group -ml-[38px]", isBroken && "cursor-not-allowed")}
|
className="group -ml-[38px]"
|
||||||
isConnectable={!isBroken}
|
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
<Dot
|
<Dot isConnected={isConnected} type={schema.type} />
|
||||||
isConnected={isConnected}
|
|
||||||
type={effectiveType}
|
|
||||||
isBroken={isBroken}
|
|
||||||
/>
|
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
@@ -132,10 +106,7 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={keyName}
|
key={keyName}
|
||||||
className={cn(
|
className="handle-container justify-end"
|
||||||
"handle-container justify-end",
|
|
||||||
isBroken && "pointer-events-none",
|
|
||||||
)}
|
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -143,16 +114,11 @@ const NodeHandle: FC<HandleProps> = ({
|
|||||||
data-testid={`output-handle-${keyName}`}
|
data-testid={`output-handle-${keyName}`}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id={keyName}
|
id={keyName}
|
||||||
className={cn("group -mr-[38px]", isBroken && "cursor-not-allowed")}
|
className="group -mr-[38px]"
|
||||||
isConnectable={!isBroken}
|
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
{label}
|
{label}
|
||||||
<Dot
|
<Dot isConnected={isConnected} type={schema.type} />
|
||||||
isConnected={isConnected}
|
|
||||||
type={effectiveType}
|
|
||||||
isBroken={isBroken}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Handle>
|
</Handle>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ConnectedEdge,
|
ConnectionData,
|
||||||
CustomNodeData,
|
CustomNodeData,
|
||||||
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
|
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
|
||||||
import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput";
|
import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput";
|
||||||
@@ -65,7 +65,7 @@ type NodeObjectInputTreeProps = {
|
|||||||
selfKey?: string;
|
selfKey?: string;
|
||||||
schema: BlockIORootSchema | BlockIOObjectSubSchema;
|
schema: BlockIORootSchema | BlockIOObjectSubSchema;
|
||||||
object?: { [key: string]: any };
|
object?: { [key: string]: any };
|
||||||
connections: ConnectedEdge[];
|
connections: ConnectionData;
|
||||||
handleInputClick: (key: string) => void;
|
handleInputClick: (key: string) => void;
|
||||||
handleInputChange: (key: string, value: any) => void;
|
handleInputChange: (key: string, value: any) => void;
|
||||||
errors: { [key: string]: string | undefined };
|
errors: { [key: string]: string | undefined };
|
||||||
@@ -585,7 +585,7 @@ const NodeOneOfDiscriminatorField: FC<{
|
|||||||
currentValue?: any;
|
currentValue?: any;
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
errors: { [key: string]: string | undefined };
|
errors: { [key: string]: string | undefined };
|
||||||
connections: ConnectedEdge[];
|
connections: ConnectionData;
|
||||||
handleInputChange: (key: string, value: any) => void;
|
handleInputChange: (key: string, value: any) => void;
|
||||||
handleInputClick: (key: string) => void;
|
handleInputClick: (key: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from "react";
|
import { FC, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import NodeHandle from "@/app/(platform)/build/components/legacy-builder/NodeHandle";
|
import NodeHandle from "@/app/(platform)/build/components/legacy-builder/NodeHandle";
|
||||||
import type {
|
import {
|
||||||
BlockIOTableSubSchema,
|
BlockIOTableSubSchema,
|
||||||
TableCellValue,
|
TableCellValue,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
import type { ConnectedEdge } from "./CustomNode/CustomNode";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "../../../../../components/atoms/Button/Button";
|
||||||
import { Input } from "@/components/atoms/Input/Input";
|
import { Input } from "../../../../../components/atoms/Input/Input";
|
||||||
|
|
||||||
interface NodeTableInputProps {
|
interface NodeTableInputProps {
|
||||||
/** Unique identifier for the node in the builder graph */
|
/** Unique identifier for the node in the builder graph */
|
||||||
@@ -26,7 +25,13 @@ interface NodeTableInputProps {
|
|||||||
/** Validation errors mapped by field key */
|
/** Validation errors mapped by field key */
|
||||||
errors: { [key: string]: string | undefined };
|
errors: { [key: string]: string | undefined };
|
||||||
/** Graph connections between nodes in the builder */
|
/** Graph connections between nodes in the builder */
|
||||||
connections: ConnectedEdge[];
|
connections: {
|
||||||
|
edge_id: string;
|
||||||
|
source: string;
|
||||||
|
sourceHandle: string;
|
||||||
|
target: string;
|
||||||
|
targetHandle: string;
|
||||||
|
}[];
|
||||||
/** Callback when table data changes */
|
/** Callback when table data changes */
|
||||||
handleInputChange: (key: string, value: TableRow[]) => void;
|
handleInputChange: (key: string, value: TableRow[]) => void;
|
||||||
/** Callback when input field is clicked (for builder selection) */
|
/** Callback when input field is clicked (for builder selection) */
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Node, Edge, useReactFlow } from "@xyflow/react";
|
import { Node, Edge, useReactFlow } from "@xyflow/react";
|
||||||
import { Key, storage } from "@/services/storage/local-storage";
|
import { Key, storage } from "@/services/storage/local-storage";
|
||||||
import { ConnectedEdge } from "./CustomNode/CustomNode";
|
|
||||||
|
|
||||||
interface CopyableData {
|
interface CopyableData {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
@@ -112,15 +111,13 @@ export function useCopyPaste(getNextNodeId: () => string) {
|
|||||||
(edge: Edge) =>
|
(edge: Edge) =>
|
||||||
edge.source === node.id || edge.target === node.id,
|
edge.source === node.id || edge.target === node.id,
|
||||||
)
|
)
|
||||||
.map(
|
.map((edge: Edge) => ({
|
||||||
(edge: Edge): ConnectedEdge => ({
|
edge_id: edge.id,
|
||||||
id: edge.id,
|
source: edge.source,
|
||||||
source: edge.source,
|
target: edge.target,
|
||||||
target: edge.target,
|
sourceHandle: edge.sourceHandle,
|
||||||
sourceHandle: edge.sourceHandle!,
|
targetHandle: edge.targetHandle,
|
||||||
targetHandle: edge.targetHandle!,
|
}));
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { GraphInputSchema } from "@/lib/autogpt-server-api";
|
|
||||||
import { GraphMetaLike, IncompatibilityInfo } from "./types";
|
|
||||||
|
|
||||||
// Helper type for schema properties - the generated types are too loose
|
|
||||||
type SchemaProperties = Record<string, GraphInputSchema["properties"][string]>;
|
|
||||||
type SchemaRequired = string[];
|
|
||||||
|
|
||||||
// Helper to safely extract schema properties
|
|
||||||
export function getSchemaProperties(schema: unknown): SchemaProperties {
|
|
||||||
if (
|
|
||||||
schema &&
|
|
||||||
typeof schema === "object" &&
|
|
||||||
"properties" in schema &&
|
|
||||||
typeof schema.properties === "object" &&
|
|
||||||
schema.properties !== null
|
|
||||||
) {
|
|
||||||
return schema.properties as SchemaProperties;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSchemaRequired(schema: unknown): SchemaRequired {
|
|
||||||
if (
|
|
||||||
schema &&
|
|
||||||
typeof schema === "object" &&
|
|
||||||
"required" in schema &&
|
|
||||||
Array.isArray(schema.required)
|
|
||||||
) {
|
|
||||||
return schema.required as SchemaRequired;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the updated agent node inputs for a sub-agent node
|
|
||||||
*/
|
|
||||||
export function createUpdatedAgentNodeInputs(
|
|
||||||
currentInputs: Record<string, unknown>,
|
|
||||||
latestSubGraphVersion: GraphMetaLike,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
...currentInputs,
|
|
||||||
graph_version: latestSubGraphVersion.version,
|
|
||||||
input_schema: latestSubGraphVersion.input_schema,
|
|
||||||
output_schema: latestSubGraphVersion.output_schema,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generic edge type that works with both builders:
|
|
||||||
* - New builder uses CustomEdge with (formally) optional handles
|
|
||||||
* - Legacy builder uses ConnectedEdge type with required handles */
|
|
||||||
export type EdgeLike = {
|
|
||||||
id: string;
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
sourceHandle?: string | null;
|
|
||||||
targetHandle?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines which edges are broken after an incompatible update.
|
|
||||||
* Works with both legacy ConnectedEdge and new CustomEdge.
|
|
||||||
*/
|
|
||||||
export function getBrokenEdgeIDs(
|
|
||||||
connections: EdgeLike[],
|
|
||||||
incompatibilities: IncompatibilityInfo,
|
|
||||||
nodeID: string,
|
|
||||||
): string[] {
|
|
||||||
const brokenEdgeIDs: string[] = [];
|
|
||||||
const typeMismatchInputNames = new Set(
|
|
||||||
incompatibilities.inputTypeMismatches.map((m) => m.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
connections.forEach((conn) => {
|
|
||||||
// Check if this connection uses a missing input (node is target)
|
|
||||||
if (
|
|
||||||
conn.target === nodeID &&
|
|
||||||
conn.targetHandle &&
|
|
||||||
incompatibilities.missingInputs.includes(conn.targetHandle)
|
|
||||||
) {
|
|
||||||
brokenEdgeIDs.push(conn.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this connection uses an input with a type mismatch (node is target)
|
|
||||||
if (
|
|
||||||
conn.target === nodeID &&
|
|
||||||
conn.targetHandle &&
|
|
||||||
typeMismatchInputNames.has(conn.targetHandle)
|
|
||||||
) {
|
|
||||||
brokenEdgeIDs.push(conn.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this connection uses a missing output (node is source)
|
|
||||||
if (
|
|
||||||
conn.source === nodeID &&
|
|
||||||
conn.sourceHandle &&
|
|
||||||
incompatibilities.missingOutputs.includes(conn.sourceHandle)
|
|
||||||
) {
|
|
||||||
brokenEdgeIDs.push(conn.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return brokenEdgeIDs;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { useSubAgentUpdate } from "./useSubAgentUpdate";
|
|
||||||
export { createUpdatedAgentNodeInputs, getBrokenEdgeIDs } from "./helpers";
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { GraphMeta as LegacyGraphMeta } from "@/lib/autogpt-server-api";
|
|
||||||
import type { GraphMeta as GeneratedGraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
|
||||||
|
|
||||||
export type SubAgentUpdateInfo<T extends GraphMetaLike = GraphMetaLike> = {
|
|
||||||
hasUpdate: boolean;
|
|
||||||
currentVersion: number;
|
|
||||||
latestVersion: number;
|
|
||||||
latestGraph: T | null;
|
|
||||||
isCompatible: boolean;
|
|
||||||
incompatibilities: IncompatibilityInfo | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Union type for GraphMeta that works with both legacy and new builder
|
|
||||||
export type GraphMetaLike = LegacyGraphMeta | GeneratedGraphMeta;
|
|
||||||
|
|
||||||
export type IncompatibilityInfo = {
|
|
||||||
missingInputs: string[]; // Connected inputs that no longer exist
|
|
||||||
missingOutputs: string[]; // Connected outputs that no longer exist
|
|
||||||
newInputs: string[]; // Inputs that exist in new version but not in current
|
|
||||||
newOutputs: string[]; // Outputs that exist in new version but not in current
|
|
||||||
newRequiredInputs: string[]; // New required inputs not in current version or not required
|
|
||||||
inputTypeMismatches: Array<{
|
|
||||||
name: string;
|
|
||||||
oldType: string;
|
|
||||||
newType: string;
|
|
||||||
}>; // Connected inputs where the type has changed
|
|
||||||
};
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
|
||||||
import { getEffectiveType } from "@/lib/utils";
|
|
||||||
import { EdgeLike, getSchemaProperties, getSchemaRequired } from "./helpers";
|
|
||||||
import {
|
|
||||||
GraphMetaLike,
|
|
||||||
IncompatibilityInfo,
|
|
||||||
SubAgentUpdateInfo,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a newer version of a sub-agent is available and determines compatibility
|
|
||||||
*/
|
|
||||||
export function useSubAgentUpdate<T extends GraphMetaLike>(
|
|
||||||
nodeID: string,
|
|
||||||
graphID: string | undefined,
|
|
||||||
graphVersion: number | undefined,
|
|
||||||
currentInputSchema: GraphInputSchema | undefined,
|
|
||||||
currentOutputSchema: GraphOutputSchema | undefined,
|
|
||||||
connections: EdgeLike[],
|
|
||||||
availableGraphs: T[],
|
|
||||||
): SubAgentUpdateInfo<T> {
|
|
||||||
// Find the latest version of the same graph
|
|
||||||
const latestGraph = useMemo(() => {
|
|
||||||
if (!graphID) return null;
|
|
||||||
return availableGraphs.find((graph) => graph.id === graphID) || null;
|
|
||||||
}, [graphID, availableGraphs]);
|
|
||||||
|
|
||||||
// Check if there's an update available
|
|
||||||
const hasUpdate = useMemo(() => {
|
|
||||||
if (!latestGraph || graphVersion === undefined) return false;
|
|
||||||
return latestGraph.version! > graphVersion;
|
|
||||||
}, [latestGraph, graphVersion]);
|
|
||||||
|
|
||||||
// Get connected input and output handles for this specific node
|
|
||||||
const connectedHandles = useMemo(() => {
|
|
||||||
const inputHandles = new Set<string>();
|
|
||||||
const outputHandles = new Set<string>();
|
|
||||||
|
|
||||||
connections.forEach((conn) => {
|
|
||||||
// If this node is the target, the targetHandle is an input on this node
|
|
||||||
if (conn.target === nodeID && conn.targetHandle) {
|
|
||||||
inputHandles.add(conn.targetHandle);
|
|
||||||
}
|
|
||||||
// If this node is the source, the sourceHandle is an output on this node
|
|
||||||
if (conn.source === nodeID && conn.sourceHandle) {
|
|
||||||
outputHandles.add(conn.sourceHandle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { inputHandles, outputHandles };
|
|
||||||
}, [connections, nodeID]);
|
|
||||||
|
|
||||||
// Check schema compatibility
|
|
||||||
const compatibilityResult = useMemo((): {
|
|
||||||
isCompatible: boolean;
|
|
||||||
incompatibilities: IncompatibilityInfo | null;
|
|
||||||
} => {
|
|
||||||
if (!hasUpdate || !latestGraph) {
|
|
||||||
return { isCompatible: true, incompatibilities: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const newInputProps = getSchemaProperties(latestGraph.input_schema);
|
|
||||||
const newOutputProps = getSchemaProperties(latestGraph.output_schema);
|
|
||||||
const newRequiredInputs = getSchemaRequired(latestGraph.input_schema);
|
|
||||||
|
|
||||||
const currentInputProps = getSchemaProperties(currentInputSchema);
|
|
||||||
const currentOutputProps = getSchemaProperties(currentOutputSchema);
|
|
||||||
const currentRequiredInputs = getSchemaRequired(currentInputSchema);
|
|
||||||
|
|
||||||
const incompatibilities: IncompatibilityInfo = {
|
|
||||||
missingInputs: [],
|
|
||||||
missingOutputs: [],
|
|
||||||
newInputs: [],
|
|
||||||
newOutputs: [],
|
|
||||||
newRequiredInputs: [],
|
|
||||||
inputTypeMismatches: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for missing connected inputs and type mismatches
|
|
||||||
connectedHandles.inputHandles.forEach((inputHandle) => {
|
|
||||||
if (!(inputHandle in newInputProps)) {
|
|
||||||
incompatibilities.missingInputs.push(inputHandle);
|
|
||||||
} else {
|
|
||||||
// Check for type mismatch on connected inputs
|
|
||||||
const currentProp = currentInputProps[inputHandle];
|
|
||||||
const newProp = newInputProps[inputHandle];
|
|
||||||
const currentType = getEffectiveType(currentProp);
|
|
||||||
const newType = getEffectiveType(newProp);
|
|
||||||
|
|
||||||
if (currentType && newType && currentType !== newType) {
|
|
||||||
incompatibilities.inputTypeMismatches.push({
|
|
||||||
name: inputHandle,
|
|
||||||
oldType: currentType,
|
|
||||||
newType: newType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for missing connected outputs
|
|
||||||
connectedHandles.outputHandles.forEach((outputHandle) => {
|
|
||||||
if (!(outputHandle in newOutputProps)) {
|
|
||||||
incompatibilities.missingOutputs.push(outputHandle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for new required inputs that didn't exist or weren't required before
|
|
||||||
newRequiredInputs.forEach((requiredInput) => {
|
|
||||||
const existedBefore = requiredInput in currentInputProps;
|
|
||||||
const wasRequiredBefore = currentRequiredInputs.includes(
|
|
||||||
requiredInput as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existedBefore || !wasRequiredBefore) {
|
|
||||||
incompatibilities.newRequiredInputs.push(requiredInput as string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for new inputs that don't exist in the current version
|
|
||||||
Object.keys(newInputProps).forEach((inputName) => {
|
|
||||||
if (!(inputName in currentInputProps)) {
|
|
||||||
incompatibilities.newInputs.push(inputName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for new outputs that don't exist in the current version
|
|
||||||
Object.keys(newOutputProps).forEach((outputName) => {
|
|
||||||
if (!(outputName in currentOutputProps)) {
|
|
||||||
incompatibilities.newOutputs.push(outputName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasIncompatibilities =
|
|
||||||
incompatibilities.missingInputs.length > 0 ||
|
|
||||||
incompatibilities.missingOutputs.length > 0 ||
|
|
||||||
incompatibilities.newRequiredInputs.length > 0 ||
|
|
||||||
incompatibilities.inputTypeMismatches.length > 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isCompatible: !hasIncompatibilities,
|
|
||||||
incompatibilities: hasIncompatibilities ? incompatibilities : null,
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
hasUpdate,
|
|
||||||
latestGraph,
|
|
||||||
currentInputSchema,
|
|
||||||
currentOutputSchema,
|
|
||||||
connectedHandles,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasUpdate,
|
|
||||||
currentVersion: graphVersion || 0,
|
|
||||||
latestVersion: latestGraph?.version || 0,
|
|
||||||
latestGraph,
|
|
||||||
isCompatible: compatibilityResult.isCompatible,
|
|
||||||
incompatibilities: compatibilityResult.incompatibilities,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
|
|
||||||
|
|
||||||
interface GraphStore {
|
interface GraphStore {
|
||||||
graphExecutionStatus: AgentExecutionStatus | undefined;
|
graphExecutionStatus: AgentExecutionStatus | undefined;
|
||||||
@@ -18,10 +17,6 @@ interface GraphStore {
|
|||||||
outputSchema: Record<string, any> | null,
|
outputSchema: Record<string, any> | null,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
// Available graphs; used for sub-graph updates
|
|
||||||
availableSubGraphs: GraphMeta[];
|
|
||||||
setAvailableSubGraphs: (graphs: GraphMeta[]) => void;
|
|
||||||
|
|
||||||
hasInputs: () => boolean;
|
hasInputs: () => boolean;
|
||||||
hasCredentials: () => boolean;
|
hasCredentials: () => boolean;
|
||||||
hasOutputs: () => boolean;
|
hasOutputs: () => boolean;
|
||||||
@@ -34,7 +29,6 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
|||||||
inputSchema: null,
|
inputSchema: null,
|
||||||
credentialsInputSchema: null,
|
credentialsInputSchema: null,
|
||||||
outputSchema: null,
|
outputSchema: null,
|
||||||
availableSubGraphs: [],
|
|
||||||
|
|
||||||
setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => {
|
setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => {
|
||||||
set({
|
set({
|
||||||
@@ -52,8 +46,6 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
|||||||
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
|
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
|
||||||
set({ inputSchema, credentialsInputSchema, outputSchema }),
|
set({ inputSchema, credentialsInputSchema, outputSchema }),
|
||||||
|
|
||||||
setAvailableSubGraphs: (graphs) => set({ availableSubGraphs: graphs }),
|
|
||||||
|
|
||||||
hasOutputs: () => {
|
hasOutputs: () => {
|
||||||
const { outputSchema } = get();
|
const { outputSchema } = get();
|
||||||
return Object.keys(outputSchema?.properties ?? {}).length > 0;
|
return Object.keys(outputSchema?.properties ?? {}).length > 0;
|
||||||
|
|||||||
@@ -17,25 +17,6 @@ import {
|
|||||||
ensurePathExists,
|
ensurePathExists,
|
||||||
parseHandleIdToPath,
|
parseHandleIdToPath,
|
||||||
} from "@/components/renderers/InputRenderer/helpers";
|
} from "@/components/renderers/InputRenderer/helpers";
|
||||||
import { IncompatibilityInfo } from "../hooks/useSubAgentUpdate/types";
|
|
||||||
|
|
||||||
// Resolution mode data stored per node
|
|
||||||
export type NodeResolutionData = {
|
|
||||||
incompatibilities: IncompatibilityInfo;
|
|
||||||
// The NEW schema from the update (what we're updating TO)
|
|
||||||
pendingUpdate: {
|
|
||||||
input_schema: Record<string, unknown>;
|
|
||||||
output_schema: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
// The OLD schema before the update (what we're updating FROM)
|
|
||||||
// Needed to merge and show removed inputs during resolution
|
|
||||||
currentSchema: {
|
|
||||||
input_schema: Record<string, unknown>;
|
|
||||||
output_schema: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
// The full updated hardcoded values to apply when resolution completes
|
|
||||||
pendingHardcodedValues: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Minimum movement (in pixels) required before logging position change to history
|
// Minimum movement (in pixels) required before logging position change to history
|
||||||
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
||||||
@@ -84,32 +65,12 @@ type NodeStore = {
|
|||||||
backendId: string,
|
backendId: string,
|
||||||
errors: { [key: string]: string },
|
errors: { [key: string]: string },
|
||||||
) => void;
|
) => void;
|
||||||
|
clearAllNodeErrors: () => void; // Add this
|
||||||
|
|
||||||
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
|
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
|
||||||
|
|
||||||
|
// Credentials optional helpers
|
||||||
setCredentialsOptional: (nodeId: string, optional: boolean) => void;
|
setCredentialsOptional: (nodeId: string, optional: boolean) => void;
|
||||||
clearAllNodeErrors: () => void;
|
|
||||||
|
|
||||||
nodesInResolutionMode: Set<string>;
|
|
||||||
brokenEdgeIDs: Map<string, Set<string>>;
|
|
||||||
nodeResolutionData: Map<string, NodeResolutionData>;
|
|
||||||
setNodeResolutionMode: (
|
|
||||||
nodeID: string,
|
|
||||||
inResolution: boolean,
|
|
||||||
resolutionData?: NodeResolutionData,
|
|
||||||
) => void;
|
|
||||||
isNodeInResolutionMode: (nodeID: string) => boolean;
|
|
||||||
getNodeResolutionData: (nodeID: string) => NodeResolutionData | undefined;
|
|
||||||
setBrokenEdgeIDs: (nodeID: string, edgeIDs: string[]) => void;
|
|
||||||
removeBrokenEdgeID: (nodeID: string, edgeID: string) => void;
|
|
||||||
isEdgeBroken: (edgeID: string) => boolean;
|
|
||||||
clearResolutionState: () => void;
|
|
||||||
|
|
||||||
isInputBroken: (nodeID: string, handleID: string) => boolean;
|
|
||||||
getInputTypeMismatch: (
|
|
||||||
nodeID: string,
|
|
||||||
handleID: string,
|
|
||||||
) => string | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||||
@@ -413,99 +374,4 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
|||||||
|
|
||||||
useHistoryStore.getState().pushState(newState);
|
useHistoryStore.getState().pushState(newState);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sub-agent resolution mode state
|
|
||||||
nodesInResolutionMode: new Set<string>(),
|
|
||||||
brokenEdgeIDs: new Map<string, Set<string>>(),
|
|
||||||
nodeResolutionData: new Map<string, NodeResolutionData>(),
|
|
||||||
|
|
||||||
setNodeResolutionMode: (
|
|
||||||
nodeID: string,
|
|
||||||
inResolution: boolean,
|
|
||||||
resolutionData?: NodeResolutionData,
|
|
||||||
) => {
|
|
||||||
set((state) => {
|
|
||||||
const newNodesSet = new Set(state.nodesInResolutionMode);
|
|
||||||
const newResolutionDataMap = new Map(state.nodeResolutionData);
|
|
||||||
const newBrokenEdgeIDs = new Map(state.brokenEdgeIDs);
|
|
||||||
|
|
||||||
if (inResolution) {
|
|
||||||
newNodesSet.add(nodeID);
|
|
||||||
if (resolutionData) {
|
|
||||||
newResolutionDataMap.set(nodeID, resolutionData);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newNodesSet.delete(nodeID);
|
|
||||||
newResolutionDataMap.delete(nodeID);
|
|
||||||
newBrokenEdgeIDs.delete(nodeID); // Clean up broken edges when exiting resolution mode
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
nodesInResolutionMode: newNodesSet,
|
|
||||||
nodeResolutionData: newResolutionDataMap,
|
|
||||||
brokenEdgeIDs: newBrokenEdgeIDs,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
isNodeInResolutionMode: (nodeID: string) => {
|
|
||||||
return get().nodesInResolutionMode.has(nodeID);
|
|
||||||
},
|
|
||||||
|
|
||||||
getNodeResolutionData: (nodeID: string) => {
|
|
||||||
return get().nodeResolutionData.get(nodeID);
|
|
||||||
},
|
|
||||||
|
|
||||||
setBrokenEdgeIDs: (nodeID: string, edgeIDs: string[]) => {
|
|
||||||
set((state) => {
|
|
||||||
const newMap = new Map(state.brokenEdgeIDs);
|
|
||||||
newMap.set(nodeID, new Set(edgeIDs));
|
|
||||||
return { brokenEdgeIDs: newMap };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeBrokenEdgeID: (nodeID: string, edgeID: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const newMap = new Map(state.brokenEdgeIDs);
|
|
||||||
const nodeSet = new Set(newMap.get(nodeID) || []);
|
|
||||||
nodeSet.delete(edgeID);
|
|
||||||
newMap.set(nodeID, nodeSet);
|
|
||||||
return { brokenEdgeIDs: newMap };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
isEdgeBroken: (edgeID: string) => {
|
|
||||||
// Check across all nodes
|
|
||||||
const brokenEdgeIDs = get().brokenEdgeIDs;
|
|
||||||
for (const edgeSet of brokenEdgeIDs.values()) {
|
|
||||||
if (edgeSet.has(edgeID)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearResolutionState: () => {
|
|
||||||
set({
|
|
||||||
nodesInResolutionMode: new Set<string>(),
|
|
||||||
brokenEdgeIDs: new Map<string, Set<string>>(),
|
|
||||||
nodeResolutionData: new Map<string, NodeResolutionData>(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Helper functions for input renderers
|
|
||||||
isInputBroken: (nodeID: string, handleID: string) => {
|
|
||||||
const resolutionData = get().nodeResolutionData.get(nodeID);
|
|
||||||
if (!resolutionData) return false;
|
|
||||||
return resolutionData.incompatibilities.missingInputs.includes(handleID);
|
|
||||||
},
|
|
||||||
|
|
||||||
getInputTypeMismatch: (nodeID: string, handleID: string) => {
|
|
||||||
const resolutionData = get().nodeResolutionData.get(nodeID);
|
|
||||||
if (!resolutionData) return undefined;
|
|
||||||
const mismatch = resolutionData.incompatibilities.inputTypeMismatches.find(
|
|
||||||
(m) => m.name === handleID,
|
|
||||||
);
|
|
||||||
return mismatch?.newType;
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function ErrorPageContent() {
|
|||||||
) {
|
) {
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
} else {
|
} else {
|
||||||
window.document.location.reload();
|
window.location.href = "/marketplace";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
href: "/profile/dashboard",
|
href: "/profile/dashboard",
|
||||||
icon: <StorefrontIcon className="size-5" />,
|
icon: <StorefrontIcon className="size-5" />,
|
||||||
},
|
},
|
||||||
...(isPaymentEnabled
|
...(isPaymentEnabled || true
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
text: "Billing",
|
text: "Billing",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const extendedButtonVariants = cva(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem] min-w-[5.5rem]",
|
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
|
||||||
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
|
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
|
||||||
icon: "p-3 !min-w-0",
|
icon: "p-3 !min-w-0",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export const FormRenderer = ({
|
|||||||
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
|
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
|
||||||
}, [preprocessedSchema, uiSchema]);
|
}, [preprocessedSchema, uiSchema]);
|
||||||
|
|
||||||
|
console.log("preprocessedSchema", preprocessedSchema);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"mb-6 mt-4"}>
|
<div className={"mb-6 mt-4"}>
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import { FieldProps, getUiOptions, getWidget } from "@rjsf/utils";
|
|||||||
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
|
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import { useAnyOfField } from "./useAnyOfField";
|
import { useAnyOfField } from "./useAnyOfField";
|
||||||
import { cleanUpHandleId, getHandleId, updateUiOption } from "../../helpers";
|
import { getHandleId, updateUiOption } from "../../helpers";
|
||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
import { ANY_OF_FLAG } from "../../constants";
|
import { ANY_OF_FLAG } from "../../constants";
|
||||||
import { findCustomFieldId } from "../../registry";
|
import { findCustomFieldId } from "../../registry";
|
||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export const AnyOfField = (props: FieldProps) => {
|
export const AnyOfField = (props: FieldProps) => {
|
||||||
const { registry, schema } = props;
|
const { registry, schema } = props;
|
||||||
@@ -23,8 +21,6 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
field_id,
|
field_id,
|
||||||
} = useAnyOfField(props);
|
} = useAnyOfField(props);
|
||||||
|
|
||||||
const isInputBroken = useNodeStore((state) => state.isInputBroken);
|
|
||||||
|
|
||||||
const parentCustomFieldId = findCustomFieldId(schema);
|
const parentCustomFieldId = findCustomFieldId(schema);
|
||||||
if (parentCustomFieldId) {
|
if (parentCustomFieldId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -47,7 +43,6 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||||
const isAnyOfInputBroken = isInputBroken(nodeId, cleanUpHandleId(handleId));
|
|
||||||
|
|
||||||
// Now anyOf can render - custom fields if the option schema matches a custom field
|
// Now anyOf can render - custom fields if the option schema matches a custom field
|
||||||
const optionCustomFieldId = optionSchema
|
const optionCustomFieldId = optionSchema
|
||||||
@@ -83,11 +78,7 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
registry={registry}
|
registry={registry}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
autocomplete={props.autocomplete}
|
autocomplete={props.autocomplete}
|
||||||
className={cn(
|
className="-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium"
|
||||||
"-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium",
|
|
||||||
isAnyOfInputBroken &&
|
|
||||||
"border-red-500 bg-red-100 text-red-600 line-through",
|
|
||||||
)}
|
|
||||||
autofocus={props.autofocus}
|
autofocus={props.autofocus}
|
||||||
label=""
|
label=""
|
||||||
hideLabel={true}
|
hideLabel={true}
|
||||||
@@ -102,7 +93,7 @@ export const AnyOfField = (props: FieldProps) => {
|
|||||||
selector={selector}
|
selector={selector}
|
||||||
uiSchema={updatedUiSchema}
|
uiSchema={updatedUiSchema}
|
||||||
/>
|
/>
|
||||||
{!isHandleConnected && !isAnyOfInputBroken && optionsSchemaField}
|
{!isHandleConnected && optionsSchemaField}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Text } from "@/components/atoms/Text/Text";
|
|||||||
import { isOptionalType } from "../../../utils/schema-utils";
|
import { isOptionalType } from "../../../utils/schema-utils";
|
||||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
|
|
||||||
interface customFieldProps extends FieldProps {
|
interface customFieldProps extends FieldProps {
|
||||||
selector: JSX.Element;
|
selector: JSX.Element;
|
||||||
@@ -52,13 +51,6 @@ export const AnyOfFieldTitle = (props: customFieldProps) => {
|
|||||||
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
|
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
|
||||||
const shoudlShowType = isHandleConnected || (isOptional && type);
|
const shoudlShowType = isHandleConnected || (isOptional && type);
|
||||||
|
|
||||||
const isInputBroken = useNodeStore((state) =>
|
|
||||||
state.isInputBroken(nodeId, cleanUpHandleId(uiOptions.handleId)),
|
|
||||||
);
|
|
||||||
const inputMismatch = useNodeStore((state) =>
|
|
||||||
state.getInputTypeMismatch(nodeId, cleanUpHandleId(uiOptions.handleId)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TitleFieldTemplate
|
<TitleFieldTemplate
|
||||||
@@ -70,16 +62,8 @@ export const AnyOfFieldTitle = (props: customFieldProps) => {
|
|||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
/>
|
/>
|
||||||
{shoudlShowType && (
|
{shoudlShowType && (
|
||||||
<Text
|
<Text variant="small" className={cn("text-zinc-700", colorClass)}>
|
||||||
variant="small"
|
{isOptional ? `(${displayType})` : "(any)"}
|
||||||
className={cn(
|
|
||||||
"text-zinc-700",
|
|
||||||
isInputBroken && "line-through",
|
|
||||||
colorClass,
|
|
||||||
inputMismatch && "rounded-md bg-red-100 px-1 !text-red-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isOptional ? `(${inputMismatch || displayType})` : "(any)"}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{shouldShowSelector && selector}
|
{shouldShowSelector && selector}
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import { Text } from "@/components/atoms/Text/Text";
|
|||||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||||
import { isAnyOfSchema } from "../../utils/schema-utils";
|
import { isAnyOfSchema } from "../../utils/schema-utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { cleanUpHandleId, isArrayItem } from "../../helpers";
|
import { isArrayItem } from "../../helpers";
|
||||||
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
|
|
||||||
export default function TitleField(props: TitleFieldProps) {
|
export default function TitleField(props: TitleFieldProps) {
|
||||||
const { id, title, required, schema, registry, uiSchema } = props;
|
const { id, title, required, schema, registry, uiSchema } = props;
|
||||||
@@ -27,11 +26,6 @@ export default function TitleField(props: TitleFieldProps) {
|
|||||||
const smallText = isArrayItemFlag || additional;
|
const smallText = isArrayItemFlag || additional;
|
||||||
|
|
||||||
const showHandle = uiOptions.showHandles ?? showHandles;
|
const showHandle = uiOptions.showHandles ?? showHandles;
|
||||||
|
|
||||||
const isInputBroken = useNodeStore((state) =>
|
|
||||||
state.isInputBroken(nodeId, cleanUpHandleId(uiOptions.handleId)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{showHandle !== false && (
|
{showHandle !== false && (
|
||||||
@@ -40,11 +34,7 @@ export default function TitleField(props: TitleFieldProps) {
|
|||||||
<Text
|
<Text
|
||||||
variant={isArrayItemFlag ? "small" : "body"}
|
variant={isArrayItemFlag ? "small" : "body"}
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn("line-clamp-1", smallText && "text-sm text-zinc-700")}
|
||||||
"line-clamp-1",
|
|
||||||
smallText && "text-sm text-zinc-700",
|
|
||||||
isInputBroken && "text-red-500 line-through",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -54,7 +44,7 @@ export default function TitleField(props: TitleFieldProps) {
|
|||||||
{!isAnyOf && (
|
{!isAnyOf && (
|
||||||
<Text
|
<Text
|
||||||
variant="small"
|
variant="small"
|
||||||
className={cn("ml-2", isInputBroken && "line-through", colorClass)}
|
className={cn("ml-2", colorClass)}
|
||||||
id={description_id}
|
id={description_id}
|
||||||
>
|
>
|
||||||
({displayType})
|
({displayType})
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ export function updateUiOption<T extends Record<string, any>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cleanUpHandleId = (handleId: string) => {
|
export const cleanUpHandleId = (handleId: string) => {
|
||||||
if (!handleId) return "";
|
|
||||||
|
|
||||||
let newHandleId = handleId;
|
let newHandleId = handleId;
|
||||||
if (handleId.includes(ANY_OF_FLAG)) {
|
if (handleId.includes(ANY_OF_FLAG)) {
|
||||||
newHandleId = newHandleId.replace(ANY_OF_FLAG, "");
|
newHandleId = newHandleId.replace(ANY_OF_FLAG, "");
|
||||||
|
|||||||
@@ -233,14 +233,13 @@ export default function useAgentGraph(
|
|||||||
title: `${block.name} ${node.id}`,
|
title: `${block.name} ${node.id}`,
|
||||||
inputSchema: block.inputSchema,
|
inputSchema: block.inputSchema,
|
||||||
outputSchema: block.outputSchema,
|
outputSchema: block.outputSchema,
|
||||||
isOutputStatic: block.staticOutput,
|
|
||||||
hardcodedValues: node.input_default,
|
hardcodedValues: node.input_default,
|
||||||
uiType: block.uiType,
|
uiType: block.uiType,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
connections: graph.links
|
connections: graph.links
|
||||||
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
|
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
|
||||||
.map((link) => ({
|
.map((link) => ({
|
||||||
id: formatEdgeID(link),
|
edge_id: formatEdgeID(link),
|
||||||
source: link.source_id,
|
source: link.source_id,
|
||||||
sourceHandle: link.source_name,
|
sourceHandle: link.source_name,
|
||||||
target: link.sink_id,
|
target: link.sink_id,
|
||||||
|
|||||||
@@ -245,8 +245,8 @@ export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
|
|||||||
// At the time of writing, combined schemas only occur on the first nested level in a
|
// At the time of writing, combined schemas only occur on the first nested level in a
|
||||||
// block schema. It is typed this way to make the use of these objects less tedious.
|
// block schema. It is typed this way to make the use of these objects less tedious.
|
||||||
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & {
|
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & {
|
||||||
type?: never;
|
type: never;
|
||||||
const?: never;
|
const: never;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
allOf: [BlockIOSimpleTypeSubSchema];
|
allOf: [BlockIOSimpleTypeSubSchema];
|
||||||
@@ -368,8 +368,8 @@ export type GraphMeta = {
|
|||||||
recommended_schedule_cron: string | null;
|
recommended_schedule_cron: string | null;
|
||||||
forked_from_id?: GraphID | null;
|
forked_from_id?: GraphID | null;
|
||||||
forked_from_version?: number | null;
|
forked_from_version?: number | null;
|
||||||
input_schema: GraphInputSchema;
|
input_schema: GraphIOSchema;
|
||||||
output_schema: GraphOutputSchema;
|
output_schema: GraphIOSchema;
|
||||||
credentials_input_schema: CredentialsInputSchema;
|
credentials_input_schema: CredentialsInputSchema;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
@@ -385,51 +385,19 @@ export type GraphMeta = {
|
|||||||
export type GraphID = Brand<string, "GraphID">;
|
export type GraphID = Brand<string, "GraphID">;
|
||||||
|
|
||||||
/* Derived from backend/data/graph.py:Graph._generate_schema() */
|
/* Derived from backend/data/graph.py:Graph._generate_schema() */
|
||||||
export type GraphInputSchema = {
|
export type GraphIOSchema = {
|
||||||
type: "object";
|
type: "object";
|
||||||
properties: Record<string, GraphInputSubSchema>;
|
properties: Record<string, GraphIOSubSchema>;
|
||||||
required: (keyof GraphInputSchema["properties"])[];
|
required: (keyof BlockIORootSchema["properties"])[];
|
||||||
};
|
};
|
||||||
export type GraphInputSubSchema = GraphOutputSubSchema &
|
export type GraphIOSubSchema = Omit<
|
||||||
(
|
BlockIOSubSchemaMeta,
|
||||||
| { type?: never; default: any | null } // AgentInputBlock (generic Any type)
|
"placeholder" | "depends_on" | "hidden"
|
||||||
| { type: "string"; format: "short-text"; default: string | null } // AgentShortTextInputBlock
|
> & {
|
||||||
| { type: "string"; format: "long-text"; default: string | null } // AgentLongTextInputBlock
|
type: never; // bodge to avoid type checking hell; doesn't exist at runtime
|
||||||
| { type: "integer"; default: number | null } // AgentNumberInputBlock
|
default?: string;
|
||||||
| { type: "string"; format: "date"; default: string | null } // AgentDateInputBlock
|
|
||||||
| { type: "string"; format: "time"; default: string | null } // AgentTimeInputBlock
|
|
||||||
| { type: "string"; format: "file"; default: string | null } // AgentFileInputBlock
|
|
||||||
| { type: "string"; enum: string[]; default: string | null } // AgentDropdownInputBlock
|
|
||||||
| { type: "boolean"; default: boolean } // AgentToggleInputBlock
|
|
||||||
| {
|
|
||||||
// AgentTableInputBlock
|
|
||||||
type: "array";
|
|
||||||
format: "table";
|
|
||||||
items: {
|
|
||||||
type: "object";
|
|
||||||
properties: Record<string, { type: "string" }>;
|
|
||||||
};
|
|
||||||
default: Array<Record<string, string>> | null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
// AgentGoogleDriveFileInputBlock
|
|
||||||
type: "object";
|
|
||||||
format: "google-drive-picker";
|
|
||||||
google_drive_picker_config?: GoogleDrivePickerConfig;
|
|
||||||
default: GoogleDriveFile | null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
export type GraphOutputSchema = {
|
|
||||||
type: "object";
|
|
||||||
properties: Record<string, GraphOutputSubSchema>;
|
|
||||||
required: (keyof GraphOutputSchema["properties"])[];
|
|
||||||
};
|
|
||||||
export type GraphOutputSubSchema = {
|
|
||||||
// TODO: typed outputs based on the incoming edges?
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
advanced: boolean;
|
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
|
metadata?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CredentialsInputSchema = {
|
export type CredentialsInputSchema = {
|
||||||
@@ -472,8 +440,8 @@ export type GraphUpdateable = Omit<
|
|||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
nodes: NodeCreatable[];
|
nodes: NodeCreatable[];
|
||||||
links: LinkCreatable[];
|
links: LinkCreatable[];
|
||||||
input_schema?: GraphInputSchema;
|
input_schema?: GraphIOSchema;
|
||||||
output_schema?: GraphOutputSchema;
|
output_schema?: GraphIOSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphCreatable = _GraphCreatableInner & {
|
export type GraphCreatable = _GraphCreatableInner & {
|
||||||
@@ -529,8 +497,8 @@ export type LibraryAgent = {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
instructions?: string | null;
|
instructions?: string | null;
|
||||||
input_schema: GraphInputSchema;
|
input_schema: GraphIOSchema;
|
||||||
output_schema: GraphOutputSchema;
|
output_schema: GraphIOSchema;
|
||||||
credentials_input_schema: CredentialsInputSchema;
|
credentials_input_schema: CredentialsInputSchema;
|
||||||
new_output: boolean;
|
new_output: boolean;
|
||||||
can_access_graph: boolean;
|
can_access_graph: boolean;
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import { NodeDimension } from "@/app/(platform)/build/components/legacy-builder/
|
|||||||
import {
|
import {
|
||||||
BlockIOObjectSubSchema,
|
BlockIOObjectSubSchema,
|
||||||
BlockIORootSchema,
|
BlockIORootSchema,
|
||||||
BlockIOSubSchema,
|
|
||||||
Category,
|
Category,
|
||||||
GraphInputSubSchema,
|
|
||||||
GraphOutputSubSchema,
|
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
@@ -79,8 +76,8 @@ export function getTypeBgColor(type: string | null): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeColor(type: string | undefined): string {
|
export function getTypeColor(type: string | null): string {
|
||||||
if (!type) return "#6b7280";
|
if (type === null) return "#6b7280";
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
string: "#22c55e",
|
string: "#22c55e",
|
||||||
@@ -91,59 +88,11 @@ export function getTypeColor(type: string | undefined): string {
|
|||||||
array: "#6366f1",
|
array: "#6366f1",
|
||||||
null: "#6b7280",
|
null: "#6b7280",
|
||||||
any: "#6b7280",
|
any: "#6b7280",
|
||||||
|
"": "#6b7280",
|
||||||
}[type] || "#6b7280"
|
}[type] || "#6b7280"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the effective type from a JSON schema, handling anyOf/oneOf/allOf wrappers.
|
|
||||||
* Returns the first non-null type found in the schema structure.
|
|
||||||
*/
|
|
||||||
export function getEffectiveType(
|
|
||||||
schema:
|
|
||||||
| BlockIOSubSchema
|
|
||||||
| GraphInputSubSchema
|
|
||||||
| GraphOutputSubSchema
|
|
||||||
| null
|
|
||||||
| undefined,
|
|
||||||
): string | undefined {
|
|
||||||
if (!schema) return undefined;
|
|
||||||
|
|
||||||
// Direct type property
|
|
||||||
if ("type" in schema && schema.type) {
|
|
||||||
return String(schema.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle allOf - typically a single-item wrapper
|
|
||||||
if (
|
|
||||||
"allOf" in schema &&
|
|
||||||
Array.isArray(schema.allOf) &&
|
|
||||||
schema.allOf.length > 0
|
|
||||||
) {
|
|
||||||
return getEffectiveType(schema.allOf[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle anyOf - e.g. [{ type: "string" }, { type: "null" }]
|
|
||||||
if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
|
|
||||||
for (const item of schema.anyOf) {
|
|
||||||
if ("type" in item && item.type !== "null") {
|
|
||||||
return String(item.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle oneOf
|
|
||||||
if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
|
|
||||||
for (const item of schema.oneOf) {
|
|
||||||
if ("type" in item && item.type !== "null") {
|
|
||||||
return String(item.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function beautifyString(name: string): string {
|
export function beautifyString(name: string): string {
|
||||||
// Regular expression to identify places to split, considering acronyms
|
// Regular expression to identify places to split, considering acronyms
|
||||||
const result = name
|
const result = name
|
||||||
|
|||||||
Reference in New Issue
Block a user